-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add command line interface to allow scripts to be run from sasview.exe #2280
Changes from 35 commits
53dc216
d73a757
3a73277
1ced3f1
31a93d2
217936b
9e09ccc
39b91f4
81ef0d3
11eb969
78dd442
c007f6e
0ed828d
4ce228a
8719156
6160896
cf9fa6c
07c3efb
999bd8d
0bffa78
d0d6414
ace822c
ddae208
58691e7
c683f64
a4e4aa2
1c8b2fb
8ad3bf1
7dbdaff
ca6864e
c848247
7e201cf
f6b25a7
fce99ae
efe6564
de26080
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
from sas.system.version import __version__ | ||
|
||
from sas.system import config, user | ||
from sas.system import config | ||
|
||
__all__ = ['config'] | ||
|
||
# TODO: fix logger-config circular dependency | ||
# Load the config file | ||
config.load() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
# | ||
#Also see sasview\src\sas\qtgui\Perspectives\Fitting\media\cli.rst | ||
# | ||
""" | ||
**SasView command line interface** | ||
|
||
This functionality is under development. Interactive sessions do not yet | ||
work in the Windows console. | ||
|
||
**Usage:** | ||
|
||
sasview [flags] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this look like once rendered by sphinx? Would expect to have seen some more formatting on these to make a good looking output following the normal conventions for commands and options, but perhaps it looks ok as it is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No idea. I can't get the docs to build on my machine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
*Run SasView. If no flag is given, or -q or -V are given, this will start | ||
the GUI.* | ||
|
||
sasview [flags] script [args...] | ||
*Run a python script using the installed SasView libraries [passing | ||
optional arguments]* | ||
|
||
sasview [flags] -m module [args...] | ||
*Run a SasView/Sasmodels/Bumps module as main [passing optional arguments]* | ||
|
||
sasview [flags] -c "python statements" [args...] | ||
*Execute python statements using the installed SasView libraries* | ||
|
||
sasview -V | ||
*Print sasview version and exit.* | ||
|
||
**Flags:** | ||
|
||
-i, --interactive. *Enter an interactive session after command/module/script.* | ||
|
||
-o, --console. *Open a console to show command output. (Windows only)* | ||
|
||
-q, --quiet. *Suppress startup messages on interactive console.* | ||
|
||
Note: On Windows any console output is ignored by default. You can either | ||
open a console to show the output with the *-o* flag or redirect output to | ||
a file using something like *sasview ... > output.txt*. | ||
""" | ||
import sys | ||
|
||
# TODO: Support dropping datafiles onto .exe? | ||
# TODO: Maybe use the bumps cli with project file as model? | ||
|
||
import argparse | ||
|
||
def parse_cli(argv): | ||
""" | ||
Parse the command argv returning an argparse.Namespace. | ||
|
||
* version: bool - print version | ||
* command: str - string to exec | ||
* module: str - module to run as main | ||
* interactive: bool - run interactive | ||
* args: list[str] - additional arguments, or script + args | ||
""" | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument("-V", "--version", action='store_true', | ||
help="Print sasview version and exit") | ||
parser.add_argument("-m", "--module", type=str, | ||
help="Run module with remaining arguments sent to main") | ||
parser.add_argument("-c", "--command", type=str, | ||
help="Execute command") | ||
parser.add_argument("-i", "--interactive", action='store_true', | ||
help="Start interactive interpreter after running command") | ||
parser.add_argument("-o", "--console", action='store_true', | ||
help="Open console to display output (windows only)") | ||
parser.add_argument("-q", "--quiet", action='store_true', | ||
help="Don't print banner when entering interactive mode") | ||
parser.add_argument("args", nargs="*", | ||
help="script followed by args") | ||
|
||
# Special case: abort argument processing after -m or -c. | ||
have_trigger = False | ||
collect_rest = False | ||
keep = [] | ||
rest = [] | ||
for arg in argv[1:]: | ||
# Append argument to the parser argv or collect them as extra args. | ||
if collect_rest: | ||
rest.append(arg) | ||
else: | ||
keep.append(arg) | ||
# For an action that needs an argument (e.g., -m module) we need | ||
# to keep the next argument for the parser, but the remaining arguments | ||
# get collected as extra args. Trigger and collect will happen in one | ||
# step if the trigger requires no args or if the arg was provided | ||
# with the trigger (e.g., -mmodule) | ||
if have_trigger: | ||
collect_rest = True | ||
if arg.startswith('-m') or arg.startswith('--module'): | ||
have_trigger = True | ||
collect_rest = arg not in ('-m', '--module') | ||
elif arg.startswith('-c') or arg.startswith('--command'): | ||
have_trigger = True | ||
collect_rest = arg not in ('-c', '--command') | ||
|
||
opts = parser.parse_args(keep) | ||
if collect_rest: | ||
opts.args = rest | ||
return opts | ||
|
||
def main(logging="production"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Likewise, passing the commandline args into it as a list of strings can make it easier to drive it for testing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function needs to hack |
||
from sas.system import log | ||
from sas.system import lib | ||
from sas.system import console | ||
|
||
# I/O redirection for the windows console. Need to do this early so that | ||
# output will be displayed on the console. Presently not working for | ||
# for production (it always opens the console even if it is not needed) | ||
# so require "sasview con ..." to open the console. Not an infamous | ||
# "temporary fix" I hope... | ||
if "-i" in sys.argv[1:] or "-o" in sys.argv[1:]: | ||
console.setup_console() | ||
|
||
# Eventually argument processing might affect logger or config, so do it first | ||
cli = parse_cli(sys.argv) | ||
|
||
# Setup logger and sasmodels | ||
if logging == "production": | ||
log.production() | ||
elif logging == "development": | ||
log.development() | ||
else: | ||
raise ValueError(f"Unknown logging mode \"{logging}\"") | ||
lib.setup_sasmodels() | ||
lib.setup_qt_env() # Note: does not import any gui libraries | ||
|
||
if cli.version: # -V | ||
import sas | ||
print(f"SasView {sas.__version__}") | ||
# Exit immediately after -V. | ||
return 0 | ||
|
||
context = {'exit': sys.exit} | ||
if cli.module: # -m module [arg...] | ||
import runpy | ||
# TODO: argv[0] should be the path to the module file not the dotted name | ||
sys.argv = [cli.module, *cli.args] | ||
context = runpy.run_module(cli.module, run_name="__main__") | ||
elif cli.command: # -c "command" | ||
sys.argv = ["-c", *cli.args] | ||
exec(cli.command, context) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will look like it succeeds no matter how well the command runs, unless an exception is raised. In terms of signalling success or failure, the likely code won't do that, but return true/false for example. Is there no prospect of meaningful error handling here so that it can be used in scripts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what you are asking for. This behaves like $ python -c "syntax error" ; echo exit status $?
File "<string>", line 1
syntax error
^
SyntaxError: invalid syntax
exit status 1
$ python run.py -c "syntax error" ; echo exit status $?
Traceback (most recent call last):
File "run.py", line 88, in <module>
sys.exit(sas.cli.main(logging="development"))
File "/Users/pkienzle/Source/sasview/src/sas/cli.py", line 131, in main
exec(cli.command, context)
File "<string>", line 1
syntax error
^
SyntaxError: invalid syntax
exit status 1
$ python -c "import sys; sys.exit(2)" ; echo exit status $?
exit status 2
$ python run.py -c "import sys; sys.exit(2)" ; echo exit status $?
exit status 2
$ python -c "True" ; echo exit status $?
exit status 0
$ python run.py -c "True" ; echo exit status $?
exit status 0 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From stackoverflow here need to use "start /wait sasview" to capture the errorlevel to the windows console for windowed applications. For example:
|
||
elif cli.args: # script [arg...] | ||
import runpy | ||
sys.argv = cli.args | ||
context = runpy.run_path(cli.args[0], run_name="__main__") | ||
elif not cli.interactive: # no arguments so start the GUI | ||
from sas.qtgui.MainWindow.MainWindow import run_sasview | ||
# sys.argv is unchanged | ||
# Maybe hand cli.quiet to run_sasview? | ||
run_sasview() | ||
return 0 # don't drop into the interactive interpreter | ||
|
||
# TODO: Start interactive with ipython rather than normal python | ||
# For ipython use: | ||
# from IPython import start_ipython | ||
# sys.argv = ["ipython", *args] | ||
# sys.exit(start_ipython()) | ||
if cli.interactive: | ||
import code | ||
# The python banner is something like | ||
# f"Python {sys.version} on {platform.system().lower()}" | ||
# where sys.version contains "VERSION (HGTAG, DATE)\n[COMPILER]" | ||
# We are replacing it with something that includes the sasview version. | ||
if cli.quiet: | ||
exitmsg = banner = "" | ||
else: | ||
import platform | ||
import sas | ||
# Form dotted python version number out of sys.version_info | ||
major, minor, micro = sys.version_info[:3] | ||
sasview_ver = f"SasView {sas.__version__}" | ||
python_ver = f"Python {major}.{minor}.{micro}" | ||
os_ver = platform.system() | ||
banner = f"{sasview_ver} for {python_ver} on {os_ver}" | ||
exitmsg = "" | ||
code.interact(banner=banner, exitmsg=exitmsg, local=context) | ||
|
||
return 0 | ||
|
||
if __name__ == "__main__": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose of this entry point as opposed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the real entrypoint. In future when we allow Maybe we should have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either this is an unneeded I'm cautious of scope creep - this PR is not about fixing the bug that there is a multiplicity of ways of starting sasview. I'm also cautious about adding yet another way of starting sasview to the set, which then needs testing and maintenance. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That leaves the
and
I find that the later is more reliable since it goes through the particular python interpreter, not whatever I'll grant that this will be more useful when we have a sasview package on pypi. Until then it requires sasview on the path, and anyone who knows how to do that can call the entry point themselves. Hint: PYTHONPATH=/path/to/sasview/src:... python -m sas.cli ... turns into PYTHONPATH=/path/to/sasview/src:... python -c "import sas.cli; sys.exit(sas.cli.main())" ... My inclination is to leave it in, but I won't object if it gets removed. |
||
sys.exit(main()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When testing locally, I often have both an installed package and a repo. If I've made changes to the repo, I'd prefer to not have to pip install for every change, so I would suggest prioritizing the repo over the installed package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently it is prioritizing the installed module. We should change that...
Also, I'm using a bare except!