Skip to content

Commit c31aeed

Browse files
authored
Merge pull request #2280 from SasView/ticket-2237-cli-2
In the absence of any contradictory response from @llimeht we have agreed to merge.
2 parents b956b61 + de26080 commit c31aeed

File tree

17 files changed

+509
-135
lines changed

17 files changed

+509
-135
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ jobs:
8080
python -m pip install wheel setuptools
8181
python -m pip install -r build_tools/requirements.txt
8282
83+
- name: Install pywin32 (Windows)
84+
if: ${{ startsWith(matrix.os, 'windows') }}
85+
run: |
86+
python -m pip install pywin32
87+
8388
- name: Install pyopencl (Windows)
8489
if: ${{ startsWith(matrix.os, 'windows') }}
8590
run: |

build_tools/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ importlib-resources
3535
bumps
3636
html2text
3737
jsonschema
38+
pywin32; platform_system == "Windows"
3839
superqt
3940
pyopengl
4041
pyopengl_accelerate

docs/sphinx-docs/source/user/working.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ Working with SasView
1818

1919
Writing a Plugin Model <qtgui/Perspectives/Fitting/plugin>
2020

21+
Scripting Interface to Sasmodels <qtgui/Perspectives/Fitting/scripting>
22+
23+
Command Line Interpreter Syntax <qtgui/Perspectives/Fitting/cli>
24+
2125
Environment Variables <environment>
2226

23-
Model marketplace <marketplace>
27+
Model Marketplace <marketplace>
2428

2529
Preferences <qtgui/MainWindow/preferences_help.rst>

installers/sasview.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
"""
77

88
import sys
9-
10-
119
sys.dont_write_bytecode = True
1210

13-
from sas.qtgui.MainWindow.MainWindow import run_sasview
14-
15-
run_sasview()
11+
if __name__ == "__main__":
12+
from multiprocessing import freeze_support
13+
freeze_support()
14+
from sas.cli import main
15+
sys.exit(main())

installers/sasview.spec

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ hiddenimports = [
6565
'uncertainties',
6666
]
6767

68+
if platform.system() == 'Windows':
69+
# Need win32 to run sasview from the command line.
70+
hiddenimports.extend([
71+
'win32',
72+
'win32.win32console',
73+
])
74+
6875
a = Analysis(
6976
['sasview.py'],
7077
pathex=[],

run.py

Lines changed: 29 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
import os
2020
from os.path import abspath, dirname, realpath, join as joinpath
2121
from contextlib import contextmanager
22-
23-
PLUGIN_MODEL_DIR = 'plugin_models'
22+
from importlib import import_module
2423

2524
def addpath(path):
2625
"""
@@ -35,7 +34,6 @@ def addpath(path):
3534
os.environ['PYTHONPATH'] = PYTHONPATH
3635
sys.path.insert(0, path)
3736

38-
3937
@contextmanager
4038
def cd(path):
4139
"""
@@ -46,62 +44,45 @@ def cd(path):
4644
yield
4745
os.chdir(old_dir)
4846

49-
def setup_sasmodels():
50-
"""
51-
Prepare sasmodels for running within sasview.
52-
"""
53-
# Set SAS_MODELPATH so sasmodels can find our custom models
54-
55-
from sas.system.user import get_user_dir
56-
plugin_dir = os.path.join(get_user_dir(), PLUGIN_MODEL_DIR)
57-
os.environ['SAS_MODELPATH'] = plugin_dir
58-
5947
def prepare():
6048
# Don't create *.pyc files
6149
sys.dont_write_bytecode = True
6250

63-
# find the directories for the source and build
64-
from distutils.util import get_platform
65-
root = abspath(dirname(realpath(__file__)))
66-
67-
platform = '%s-%s' % (get_platform(), sys.version[:3])
68-
build_path = joinpath(root, 'build', 'lib.' + platform)
51+
# Turn numpy warnings into errors
52+
#import numpy; numpy.seterr(all='raise')
6953

70-
# Notify the help menu that the Sphinx documentation is in a different
71-
# place than it otherwise would be.
72-
os.environ['SASVIEW_DOC_PATH'] = joinpath(build_path, "doc")
73-
74-
try:
75-
import periodictable
76-
except ImportError:
77-
addpath(joinpath(root, '..', 'periodictable'))
54+
# Find the directories for the source and build
55+
root = abspath(dirname(realpath(__file__)))
7856

79-
try:
80-
import bumps
81-
except ImportError:
82-
addpath(joinpath(root, '..', 'bumps'))
57+
# TODO: Do we prioritize the sibling repo or the installed package?
58+
# TODO: Can we use sasview/run.py from a distributed sasview.exe?
59+
# Put supporting packages on the path if they are not already available.
60+
for sibling in ('periodictable', 'bumps', 'sasdata', 'sasmodels'):
61+
try:
62+
import_module(sibling)
63+
except:
64+
addpath(joinpath(root, '..', sibling))
8365

8466
# Put the source trees on the path
8567
addpath(joinpath(root, 'src'))
8668

87-
# sasmodels on the path
88-
addpath(joinpath(root, '../sasmodels/'))
89-
69+
# == no more C sources so no need to build project to run it ==
70+
# Leave this snippet around in case we add a compile step later.
71+
#from distutils.util import get_platform
72+
#platform = '%s-%s' % (get_platform(), sys.version[:3])
73+
#build_path = joinpath(root, 'build', 'lib.' + platform)
74+
## Build project if the build directory does not already exist.
75+
#if not os.path.exists(build_path):
76+
# import subprocess
77+
# with cd(root):
78+
# subprocess.call((sys.executable, "setup.py", "build"), shell=False)
9079

80+
# Notify the help menu that the Sphinx documentation is in a different
81+
# place than it otherwise would be.
82+
docpath = joinpath(root, 'docs', 'sphinx-docs', '_build', 'html')
83+
os.environ['SASVIEW_DOC_PATH'] = docpath
9184

9285
if __name__ == "__main__":
93-
# Need to add absolute path before actual prepare call,
94-
# so logging can be done during initialization process too
95-
root = abspath(dirname(realpath(sys.argv[0])))
96-
97-
addpath(joinpath(root, 'src'))
9886
prepare()
99-
100-
# Run the UI conversion tool when executed from script. This has to
101-
# happen after prepare() so that sas.qtgui is on the path.
102-
import sas.qtgui.convertUI
103-
setup_sasmodels()
104-
105-
from sas.qtgui.MainWindow.MainWindow import run_sasview
106-
run_sasview()
107-
#logger.debug("Ending SASVIEW in debug mode.")
87+
import sas.cli
88+
sys.exit(sas.cli.main(logging="development"))

src/sas/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from sas.system.version import __version__
2-
3-
from sas.system import config, user
2+
from sas.system import config
43

54
__all__ = ['config']
65

6+
# TODO: fix logger-config circular dependency
77
# Load the config file
88
config.load()
99

src/sas/cli.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#
2+
#Also see sasview\src\sas\qtgui\Perspectives\Fitting\media\cli.rst
3+
#
4+
"""
5+
**SasView command line interface**
6+
7+
This functionality is under development. Interactive sessions do not yet
8+
work in the Windows console.
9+
10+
**Usage:**
11+
12+
sasview [flags]
13+
*Run SasView. If no flag is given, or -q or -V are given, this will start
14+
the GUI.*
15+
16+
sasview [flags] script [args...]
17+
*Run a python script using the installed SasView libraries [passing
18+
optional arguments]*
19+
20+
sasview [flags] -m module [args...]
21+
*Run a SasView/Sasmodels/Bumps module as main [passing optional arguments]*
22+
23+
sasview [flags] -c "python statements" [args...]
24+
*Execute python statements using the installed SasView libraries*
25+
26+
sasview -V
27+
*Print sasview version and exit.*
28+
29+
**Flags:**
30+
31+
-i, --interactive. *Enter an interactive session after command/module/script.*
32+
33+
-o, --console. *Open a console to show command output. (Windows only)*
34+
35+
-q, --quiet. *Suppress startup messages on interactive console.*
36+
37+
Note: On Windows any console output is ignored by default. You can either
38+
open a console to show the output with the *-o* flag or redirect output to
39+
a file using something like *sasview ... > output.txt*.
40+
"""
41+
import sys
42+
43+
# TODO: Support dropping datafiles onto .exe?
44+
# TODO: Maybe use the bumps cli with project file as model?
45+
46+
import argparse
47+
48+
def parse_cli(argv):
49+
"""
50+
Parse the command argv returning an argparse.Namespace.
51+
52+
* version: bool - print version
53+
* command: str - string to exec
54+
* module: str - module to run as main
55+
* interactive: bool - run interactive
56+
* args: list[str] - additional arguments, or script + args
57+
"""
58+
parser = argparse.ArgumentParser()
59+
parser.add_argument("-V", "--version", action='store_true',
60+
help="Print sasview version and exit")
61+
parser.add_argument("-m", "--module", type=str,
62+
help="Run module with remaining arguments sent to main")
63+
parser.add_argument("-c", "--command", type=str,
64+
help="Execute command")
65+
parser.add_argument("-i", "--interactive", action='store_true',
66+
help="Start interactive interpreter after running command")
67+
parser.add_argument("-o", "--console", action='store_true',
68+
help="Open console to display output (windows only)")
69+
parser.add_argument("-q", "--quiet", action='store_true',
70+
help="Don't print banner when entering interactive mode")
71+
parser.add_argument("args", nargs="*",
72+
help="script followed by args")
73+
74+
# Special case: abort argument processing after -m or -c.
75+
have_trigger = False
76+
collect_rest = False
77+
keep = []
78+
rest = []
79+
for arg in argv[1:]:
80+
# Append argument to the parser argv or collect them as extra args.
81+
if collect_rest:
82+
rest.append(arg)
83+
else:
84+
keep.append(arg)
85+
# For an action that needs an argument (e.g., -m module) we need
86+
# to keep the next argument for the parser, but the remaining arguments
87+
# get collected as extra args. Trigger and collect will happen in one
88+
# step if the trigger requires no args or if the arg was provided
89+
# with the trigger (e.g., -mmodule)
90+
if have_trigger:
91+
collect_rest = True
92+
if arg.startswith('-m') or arg.startswith('--module'):
93+
have_trigger = True
94+
collect_rest = arg not in ('-m', '--module')
95+
elif arg.startswith('-c') or arg.startswith('--command'):
96+
have_trigger = True
97+
collect_rest = arg not in ('-c', '--command')
98+
99+
opts = parser.parse_args(keep)
100+
if collect_rest:
101+
opts.args = rest
102+
return opts
103+
104+
def main(logging="production"):
105+
from sas.system import log
106+
from sas.system import lib
107+
from sas.system import console
108+
109+
# I/O redirection for the windows console. Need to do this early so that
110+
# output will be displayed on the console. Presently not working for
111+
# for production (it always opens the console even if it is not needed)
112+
# so require "sasview con ..." to open the console. Not an infamous
113+
# "temporary fix" I hope...
114+
if "-i" in sys.argv[1:] or "-o" in sys.argv[1:]:
115+
console.setup_console()
116+
117+
# Eventually argument processing might affect logger or config, so do it first
118+
cli = parse_cli(sys.argv)
119+
120+
# Setup logger and sasmodels
121+
if logging == "production":
122+
log.production()
123+
elif logging == "development":
124+
log.development()
125+
else:
126+
raise ValueError(f"Unknown logging mode \"{logging}\"")
127+
lib.setup_sasmodels()
128+
lib.setup_qt_env() # Note: does not import any gui libraries
129+
130+
if cli.version: # -V
131+
import sas
132+
print(f"SasView {sas.__version__}")
133+
# Exit immediately after -V.
134+
return 0
135+
136+
context = {'exit': sys.exit}
137+
if cli.module: # -m module [arg...]
138+
import runpy
139+
# TODO: argv[0] should be the path to the module file not the dotted name
140+
sys.argv = [cli.module, *cli.args]
141+
context = runpy.run_module(cli.module, run_name="__main__")
142+
elif cli.command: # -c "command"
143+
sys.argv = ["-c", *cli.args]
144+
exec(cli.command, context)
145+
elif cli.args: # script [arg...]
146+
import runpy
147+
sys.argv = cli.args
148+
context = runpy.run_path(cli.args[0], run_name="__main__")
149+
elif not cli.interactive: # no arguments so start the GUI
150+
from sas.qtgui.MainWindow.MainWindow import run_sasview
151+
# sys.argv is unchanged
152+
# Maybe hand cli.quiet to run_sasview?
153+
run_sasview()
154+
return 0 # don't drop into the interactive interpreter
155+
156+
# TODO: Start interactive with ipython rather than normal python
157+
# For ipython use:
158+
# from IPython import start_ipython
159+
# sys.argv = ["ipython", *args]
160+
# sys.exit(start_ipython())
161+
if cli.interactive:
162+
import code
163+
# The python banner is something like
164+
# f"Python {sys.version} on {platform.system().lower()}"
165+
# where sys.version contains "VERSION (HGTAG, DATE)\n[COMPILER]"
166+
# We are replacing it with something that includes the sasview version.
167+
if cli.quiet:
168+
exitmsg = banner = ""
169+
else:
170+
import platform
171+
import sas
172+
# Form dotted python version number out of sys.version_info
173+
major, minor, micro = sys.version_info[:3]
174+
sasview_ver = f"SasView {sas.__version__}"
175+
python_ver = f"Python {major}.{minor}.{micro}"
176+
os_ver = platform.system()
177+
banner = f"{sasview_ver} for {python_ver} on {os_ver}"
178+
exitmsg = ""
179+
code.interact(banner=banner, exitmsg=exitmsg, local=context)
180+
181+
return 0
182+
183+
if __name__ == "__main__":
184+
sys.exit(main())

0 commit comments

Comments
 (0)