From 53dc2160a4d70464769f0c1b806c6496cf32c592 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 25 Oct 2022 16:51:18 -0400 Subject: [PATCH 01/28] Add command line interface to allow scripts to be run from sasview.exe --- installers/sasview.py | 10 +-- run.py | 77 ++++++---------- src/sas/__init__.py | 4 +- src/sas/cli.py | 90 +++++++++++++++++++ src/sas/qtgui/MainWindow/MainWindow.py | 35 ++------ .../qtgui/Perspectives/Fitting/GPUOptions.py | 7 +- src/sas/system/__init__.py | 1 + src/sas/system/env.py | 1 + src/sas/system/lib.py | 41 +++++++++ src/sas/system/log.py | 6 ++ 10 files changed, 185 insertions(+), 87 deletions(-) create mode 100644 src/sas/cli.py create mode 100644 src/sas/system/lib.py diff --git a/installers/sasview.py b/installers/sasview.py index 9e3ad44ef9..fe5c14bdc4 100644 --- a/installers/sasview.py +++ b/installers/sasview.py @@ -6,10 +6,10 @@ """ import sys - - sys.dont_write_bytecode = True -from sas.qtgui.MainWindow.MainWindow import run_sasview - -run_sasview() +if __name__ == "__main__": + from multiprocessing import freeze_support + freeze_support() + from sas.cli import main + main() diff --git a/run.py b/run.py index 4fd65f4c53..74ae591855 100644 --- a/run.py +++ b/run.py @@ -19,8 +19,7 @@ import os from os.path import abspath, dirname, realpath, join as joinpath from contextlib import contextmanager - -PLUGIN_MODEL_DIR = 'plugin_models' +from importlib import import_module def addpath(path): """ @@ -35,7 +34,6 @@ def addpath(path): os.environ['PYTHONPATH'] = PYTHONPATH sys.path.insert(0, path) - @contextmanager def cd(path): """ @@ -46,62 +44,45 @@ def cd(path): yield os.chdir(old_dir) -def setup_sasmodels(): - """ - Prepare sasmodels for running within sasview. - """ - # Set SAS_MODELPATH so sasmodels can find our custom models - - from sas.system.user import get_user_dir - plugin_dir = os.path.join(get_user_dir(), PLUGIN_MODEL_DIR) - os.environ['SAS_MODELPATH'] = plugin_dir - def prepare(): # Don't create *.pyc files sys.dont_write_bytecode = True - # find the directories for the source and build - from distutils.util import get_platform - root = abspath(dirname(realpath(__file__))) - - platform = '%s-%s' % (get_platform(), sys.version[:3]) - build_path = joinpath(root, 'build', 'lib.' + platform) + # Turn numpy warnings into errors + #import numpy; numpy.seterr(all='raise') - # Notify the help menu that the Sphinx documentation is in a different - # place than it otherwise would be. - os.environ['SASVIEW_DOC_PATH'] = joinpath(build_path, "doc") - - try: - import periodictable - except ImportError: - addpath(joinpath(root, '..', 'periodictable')) + # Find the directories for the source and build + root = abspath(dirname(realpath(__file__))) - try: - import bumps - except ImportError: - addpath(joinpath(root, '..', 'bumps')) + # TODO: Do we prioritize the sister repo or the installed package? + # TODO: Can we use sasview/run.py from a distributed sasview.exe? + # Put supporting packages on the path if they are not already available. + for sister in ('periodictable', 'bumps', 'sasdata', 'sasmodels'): + try: + import_module(sister) + except: + addpath(joinpath(root, '..', sister)) # Put the source trees on the path addpath(joinpath(root, 'src')) - # sasmodels on the path - addpath(joinpath(root, '../sasmodels/')) - + # == no more C sources so no need to build project to run it == + # Leave this snippet around in case we add a compile step later. + #from distutils.util import get_platform + #platform = '%s-%s' % (get_platform(), sys.version[:3]) + #build_path = joinpath(root, 'build', 'lib.' + platform) + ## Build project if the build directory does not already exist. + #if not os.path.exists(build_path): + # import subprocess + # with cd(root): + # subprocess.call((sys.executable, "setup.py", "build"), shell=False) + # Notify the help menu that the Sphinx documentation is in a different + # place than it otherwise would be. + docpath = joinpath(root, 'docs', 'sphinx-docs', '_build', 'html') + os.environ['SASVIEW_DOC_PATH'] = docpath if __name__ == "__main__": - # Need to add absolute path before actual prepare call, - # so logging can be done during initialization process too - root = abspath(dirname(realpath(sys.argv[0]))) - - addpath(joinpath(root, 'src')) prepare() - - # Run the UI conversion tool when executed from script. This has to - # happen after prepare() so that sas.qtgui is on the path. - import sas.qtgui.convertUI - setup_sasmodels() - - from sas.qtgui.MainWindow.MainWindow import run_sasview - run_sasview() - #logger.debug("Ending SASVIEW in debug mode.") + import sas.cli + sas.cli.main(logging="development") diff --git a/src/sas/__init__.py b/src/sas/__init__.py index f0cd9a8647..81b430332d 100644 --- a/src/sas/__init__.py +++ b/src/sas/__init__.py @@ -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() diff --git a/src/sas/cli.py b/src/sas/cli.py new file mode 100644 index 0000000000..ec2ab9bd6d --- /dev/null +++ b/src/sas/cli.py @@ -0,0 +1,90 @@ +""" +SasView command line interface. + +sasview -m module [args...] + Run module as main. +sasview -c "python statements" + Execute python statements with sasview libraries available. +sasview -i + Start ipython interpreter. +sasview script [args...] + Run script with sasview libraries available +sasview + Start sasview gui +""" +import sys + +# TODO: Support dropping datafiles onto .exe? +# TODO: Maybe use the bumps cli with project file as model? + +import argparse +parser = argparse.ArgumentParser() +parser.add_argument("-m", "--module", type=str, + help="Run module as main") +parser.add_argument("-c", "--command", type=str, + help="Execute command") +parser.add_argument("-i", "--interactive", action='store_true', + help="Run interactive command line") +parser.add_argument("argv", nargs="*", + help="script followed by argv") + +def exclusive_error(): + print("Use only one of -m module args, -c command, -i, or script.py args.", sys.stderr) + sys.exit(1) + +def run_interactive(): + """Run sasview as an interactive python interpreter""" + try: + from IPython import start_ipython + sys.argv = ["ipython", "--pylab"] + sys.exit(start_ipython()) + except ImportError: + import code + code.interact(local={'exit': sys.exit}) + +def main(logging="production"): + from sas.system import log + from sas.system import lib + + # Eventually argument processing might affect logger or config, so do it first + args = parser.parse_args() + + # 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() + + # Parse mutually exclusive command line options + # mutually exclusive (-m module args, -c command, -i, script args) + if args.argv and not args.module: # script [arg...] + import runpy + if args.command or args.module or args.interactive: + exclusive_error() + sys.argv = args.argv + runpy.run_path(args.argv[0], run_name="__main__") + elif args.module: # -m module [arg...] + import runpy + if args.command or args.interactive: + exclusive_error() + sys.argv = [args.module, *args.argv] + runpy.run_module(args.module, run_name="__main__") + elif args.command: # -c "command" + if args.argv or args.module or args.interactive: + exclusive_error() + exec(args.command) + elif args.interactive: # -i + if args.argv or args.module or args.command: + exclusive_error() + run_interactive() + else: + from sas.qtgui.MainWindow.MainWindow import run_sasview as run_gui + run_gui() + +if __name__ == "__main__": + from multiprocessing import freeze_support + freeze_support() + main() diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index 875332ea1f..d10c8ee214 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -1,11 +1,10 @@ # UNLESS EXEPTIONALLY REQUIRED TRY TO AVOID IMPORTING ANY MODULES HERE # ESPECIALLY ANYTHING IN SAS, SASMODELS NAMESPACE -import logging import os import sys +import logging -from sas import config -from sas.system import env, version +from sas.system.version import __version__ from PyQt5.QtWidgets import QMainWindow from PyQt5.QtWidgets import QMdiArea @@ -17,6 +16,8 @@ # Local UI from .UI.MainWindowUI import Ui_SasView +logger = logging.getLogger(__name__) + class MainSasViewWindow(QMainWindow, Ui_SasView): # Main window of the application def __init__(self, screen_resolution, parent=None): @@ -25,7 +26,7 @@ def __init__(self, screen_resolution, parent=None): self.setupUi(self) # Add the version number to window title - self.setWindowTitle(f"SasView {version.__version__}") + self.setWindowTitle(f"SasView {__version__}") # define workspace for dialogs. self.workspace = QMdiArea(self) # some perspectives are fixed size. @@ -45,8 +46,7 @@ def __init__(self, screen_resolution, parent=None): try: self.guiManager = GuiManager(self) except Exception as ex: - import logging - logging.error("Application failed with: "+str(ex)) + logger.error("Application failed with: "+str(ex)) raise ex def closeEvent(self, event): @@ -68,31 +68,12 @@ def SplashScreen(): return splashScreen def run_sasview(): - app = QApplication([]) - - #Initialize logger - from sas.system.log import SetupLogger - SetupLogger(__name__).config_development() - - # initialize sasmodels settings - from sas.system.user import get_user_dir - if "SAS_DLL_PATH" not in os.environ: - os.environ["SAS_DLL_PATH"] = os.path.join( - get_user_dir(), "compiled_models") - - # Set open cl config from environment variable, if it is set - - if env.sas_opencl is not None: - logging.getLogger(__name__).info("Getting OpenCL settings from environment variables") - config.SAS_OPENCL = env.sas_opencl - else: - logging.getLogger(__name__).info("Getting OpenCL settings from config") - env.sas_opencl = config.SAS_OPENCL - # Make the event loop interruptable quickly import signal signal.signal(signal.SIGINT, signal.SIG_DFL) + app = QApplication([]) + # Main must have reference to the splash screen, so making it explicit splash = SplashScreen() splash.show() diff --git a/src/sas/qtgui/Perspectives/Fitting/GPUOptions.py b/src/sas/qtgui/Perspectives/Fitting/GPUOptions.py index 0297630002..30eb718d35 100644 --- a/src/sas/qtgui/Perspectives/Fitting/GPUOptions.py +++ b/src/sas/qtgui/Perspectives/Fitting/GPUOptions.py @@ -18,8 +18,8 @@ from sas.qtgui.Perspectives.Fitting.UI.GPUOptionsUI import Ui_GPUOptions from sas.qtgui.Perspectives.Fitting.UI.GPUTestResultsUI import Ui_GPUTestResults -from sas.system import env from sas import config +from sas.system import lib try: _fromUtf8 = QtCore.QString.fromUtf8 @@ -107,11 +107,8 @@ def set_sas_open_cl(self): sas_open_cl = self.cl_options[str(checked.text())] no_opencl_msg = sas_open_cl.lower() == "none" - env.sas_opencl = sas_open_cl - config.SAS_OPENCL = sas_open_cl + lib.reset_sasmodels(sas_open_cl) - # CRUFT: next version of reset_environment() will return env - sasmodels.sasview_model.reset_environment() return no_opencl_msg def testButtonClicked(self): diff --git a/src/sas/system/__init__.py b/src/sas/system/__init__.py index 5b84646217..4e42ad94fd 100644 --- a/src/sas/system/__init__.py +++ b/src/sas/system/__init__.py @@ -1,3 +1,4 @@ +# TODO: Don't shadow module names; it makes debugging difficult. from .web import web from .legal import legal from .env import env diff --git a/src/sas/system/env.py b/src/sas/system/env.py index 35da0511ba..c595d305d0 100644 --- a/src/sas/system/env.py +++ b/src/sas/system/env.py @@ -1,3 +1,4 @@ +# ** DEPRECATED ** """ Interface for environment variable access This is intended to handle any conversion from the environment variable string to more natural types. diff --git a/src/sas/system/lib.py b/src/sas/system/lib.py new file mode 100644 index 0000000000..a12b4b787c --- /dev/null +++ b/src/sas/system/lib.py @@ -0,0 +1,41 @@ +# Setup third-party libraries (e.g., sasview, periodictable, bumps) +import os + +# TODO: Add api to control sasmodels rather than using environment variables +def setup_sasmodels(): + """Initialize sasmodels settings""" + from .user import get_user_dir + + # Don't need to set SAS_MODELPATH for gui because sascalc.fit uses the + # full paths to models, but when using the sasview package as a python + # distribution for running sasmodels scripts we need to set SAS_MODELPATH + # to the path used by SasView to store models. + from sas.sascalc.fit.models import find_plugins_dir + os.environ['SAS_MODELPATH'] = find_plugins_dir() + + # TODO: Use same mechanism as OpenCL/CUDA to manage the cache file path + # Both scripts and gui need to know the stored DLL path. + if "SAS_DLL_PATH" not in os.environ: + os.environ["SAS_DLL_PATH"] = os.path.join( + get_user_dir(), "compiled_models") + + # Set OpenCL config from environment variable if it is set otherwise + # use the value from the sas config file. + from sas import config + # Not using sas.system.env since it just adds a layer of confusion + SAS_OPENCL = os.environ.get("SAS_OPENCL", None) + if SAS_OPENCL is None: + # Let sasmodels know the value of the config variable + os.environ["SAS_OPENCL"] = config.SAS_OPENCL + else: + # Let config system know the value of the the environment variable + config.SAS_OPENCL = SAS_OPENCL + +def reset_sasmodels(sas_opencl): + from sasmodels.sasview_model import reset_environment + from sas import config + + config.SAS_OPENCL = sas_opencl + os.environ["SAS_OPENCL"] = sas_opencl + # CRUFT: next version of reset_environment() will return env + reset_environment() diff --git a/src/sas/system/log.py b/src/sas/system/log.py index 77dabb0362..9ebedd1d6a 100644 --- a/src/sas/system/log.py +++ b/src/sas/system/log.py @@ -84,3 +84,9 @@ def _find_config_file(self, filename="log.ini"): return print(f"'{filename}' not found.", file=sys.stderr) self.config_file = None + +def production(): + return SetupLogger("sasview").config_production() + +def development(): + return SetupLogger("sasview").config_development() From 3a7327740ba7152a52987c62a8bec02792327f5b Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 08:49:30 -0400 Subject: [PATCH 02/28] Included win32 in the sasview distribution --- installers/sasview.spec | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/installers/sasview.spec b/installers/sasview.spec index 7d38396ceb..e29c242995 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -1,6 +1,7 @@ # -*- mode: python ; coding: utf-8 -*- import sys +import os from pathlib import Path import warnings import platform @@ -65,6 +66,10 @@ hiddenimports = [ 'uncertainties', ] +if os.name == 'nt': + # Need win32 to run sasview from the command line. + hiddenimports.append('win32') + a = Analysis( ['sasview.py'], pathex=[], From 31a93d2a83628de8992b90b0eb96c4fea2e05b94 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 12:40:14 -0400 Subject: [PATCH 03/28] freeze support is only needed for the sasview.py exe script; remove it from sas.cli --- src/sas/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index ec2ab9bd6d..50415deb03 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -11,6 +11,8 @@ Run script with sasview libraries available sasview Start sasview gui + +You can also run it as "python -m sas.cli". """ import sys @@ -85,6 +87,4 @@ def main(logging="production"): run_gui() if __name__ == "__main__": - from multiprocessing import freeze_support - freeze_support() main() From 217936b0c830d7fecbf3a8684a71069462bec748 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 12:49:36 -0400 Subject: [PATCH 04/28] fix merge bug; sas.system.config is needed again --- src/sas/qtgui/MainWindow/MainWindow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index 26028a88de..db2df1df68 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -5,6 +5,7 @@ import logging from sas.system.version import __version__ +from sas.system import config from PyQt5.QtWidgets import QMainWindow from PyQt5.QtWidgets import QMdiArea @@ -78,7 +79,7 @@ def run_sasview(): os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" os.environ["QT_SCALE_FACTOR"] = f"{config.QT_SCALE_FACTOR}" os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" if config.QT_AUTO_SCREEN_SCALE_FACTOR else "0" - + app = QApplication([]) # Main must have reference to the splash screen, so making it explicit From 9e09ccc6221fb5fdaf2465ea7d80015f1af679b6 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 12:50:02 -0400 Subject: [PATCH 05/28] sasview cli needs win32.win32console --- installers/sasview.spec | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/installers/sasview.spec b/installers/sasview.spec index e29c242995..ebad962127 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -68,7 +68,10 @@ hiddenimports = [ if os.name == 'nt': # Need win32 to run sasview from the command line. - hiddenimports.append('win32') + hiddenimports.extend([ + 'win32', + 'win32.win32console', + ]) a = Analysis( ['sasview.py'], From 39b91f47ddae9937ef03b010156a886a5b9cf5de Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 13:22:29 -0400 Subject: [PATCH 06/28] use consistent platform query method --- installers/sasview.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/sasview.spec b/installers/sasview.spec index ebad962127..54f4b00c93 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -66,7 +66,7 @@ hiddenimports = [ 'uncertainties', ] -if os.name == 'nt': +if platform.system() == 'Windows': # Need win32 to run sasview from the command line. hiddenimports.extend([ 'win32', From 81ef0d39306b1024a2e9637eb80aea0682c63ad2 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 13:33:36 -0400 Subject: [PATCH 07/28] Add win32 as required package --- .github/workflows/installers.yml | 2 +- .github/workflows/release.yml | 3 +-- build_tools/requirements.txt | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml index 999b818e8d..7803ff89f1 100644 --- a/.github/workflows/installers.yml +++ b/.github/workflows/installers.yml @@ -91,7 +91,7 @@ jobs: - name: Install pyopencl (Windows) if: ${{ matrix.os == 'windows-latest' }} run: | - python -m pip install pytools mako cffi + python -m pip install pytools mako cffi win32 choco install opencl-intel-cpu-runtime choco install innosetup python -m pip install --only-binary=pyopencl --find-links http://www.silx.org/pub/wheelhouse/ --trusted-host www.silx.org pyopencl diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea151eadb9..9fb1d7b8a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,7 +70,6 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install wheel setuptools - python -m pip install numpy scipy==1.7.3 docutils "pytest<6" sphinx unittest-xml-reporting python -m pip install tinycc h5py sphinx pyparsing html5lib reportlab==3.6.6 pybind11 appdirs python -m pip install six numba mako ipython qtconsole xhtml2pdf unittest-xml-reporting pylint @@ -92,7 +91,7 @@ jobs: - name: Install pyopencl (Windows) if: ${{ matrix.os == 'windows-latest' }} run: | - python -m pip install pytools mako cffi + python -m pip install pytools mako cffi win32 choco install opencl-intel-cpu-runtime choco install innosetup python -m pip install --only-binary=pyopencl --find-links http://www.silx.org/pub/wheelhouse/ --trusted-host www.silx.org pyopencl diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt index badbfd0bc5..f6b6fcaf67 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -34,3 +34,4 @@ importlib-resources bumps html2text jsonschema +win32; platform_system == "Windows" From 11eb9690faca84b3b77f4edad9b3d7ff89aff110 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 15:27:18 -0400 Subject: [PATCH 08/28] Note that Qt environment variables may be needed for scripts --- src/sas/qtgui/MainWindow/MainWindow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index db2df1df68..ca2ec2a846 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -76,6 +76,9 @@ def run_sasview(): import signal signal.signal(signal.SIGINT, signal.SIG_DFL) + # TODO: Move to sas.cli.main if needed for scripts. + # sas.cli.main does not yet import sas.config so leave here until needed. + # Alternative: define a qt setup function that can be called from a script. os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" os.environ["QT_SCALE_FACTOR"] = f"{config.QT_SCALE_FACTOR}" os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" if config.QT_AUTO_SCREEN_SCALE_FACTOR else "0" From 78dd442d264334d75e9606de5f557e764a8d33e7 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 15:28:15 -0400 Subject: [PATCH 09/28] redo command line processor so that it stops on -i/-c/-m --- src/sas/cli.py | 118 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 39 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index 50415deb03..85005309f8 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -1,12 +1,14 @@ """ SasView command line interface. +sasview -V + Print SasView version and exit. sasview -m module [args...] Run module as main. -sasview -c "python statements" +sasview -c "python statements" [args...] Execute python statements with sasview libraries available. -sasview -i - Start ipython interpreter. +sasview -i [args...] + Start ipython interpreter using args. sasview script [args...] Run script with sasview libraries available sasview @@ -20,28 +22,70 @@ # TODO: Maybe use the bumps cli with project file as model? import argparse -parser = argparse.ArgumentParser() -parser.add_argument("-m", "--module", type=str, - help="Run module as main") -parser.add_argument("-c", "--command", type=str, - help="Execute command") -parser.add_argument("-i", "--interactive", action='store_true', - help="Run interactive command line") -parser.add_argument("argv", nargs="*", - help="script followed by argv") -def exclusive_error(): - print("Use only one of -m module args, -c command, -i, or script.py args.", sys.stderr) - sys.exit(1) +def parse_cli(argv): + """ + Parse the command argv returning an argparse.Namespace. -def run_interactive(): + * 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 ipython with remaining arguments") + parser.add_argument("args", nargs="*", + help="script followed by args") + + # Special case: abort argument processing after -i, -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 + elif arg in ('-i', '--interactive'): + have_trigger = collect_rest = True + elif 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 run_interactive(args): """Run sasview as an interactive python interpreter""" try: from IPython import start_ipython - sys.argv = ["ipython", "--pylab"] + sys.argv = ["ipython", "--pylab", *args] sys.exit(start_ipython()) except ImportError: import code + sys.argv = args code.interact(local={'exit': sys.exit}) def main(logging="production"): @@ -49,7 +93,7 @@ def main(logging="production"): from sas.system import lib # Eventually argument processing might affect logger or config, so do it first - args = parser.parse_args() + cli = parse_cli(sys.argv) # Setup logger and sasmodels if logging == "production": @@ -60,30 +104,26 @@ def main(logging="production"): raise ValueError(f"Unknown logging mode \"{logging}\"") lib.setup_sasmodels() - # Parse mutually exclusive command line options - # mutually exclusive (-m module args, -c command, -i, script args) - if args.argv and not args.module: # script [arg...] + if cli.version: # -V + import sas + print(f"SasView {sas.__version__}") + elif cli.module: # -m module [arg...] import runpy - if args.command or args.module or args.interactive: - exclusive_error() - sys.argv = args.argv - runpy.run_path(args.argv[0], run_name="__main__") - elif args.module: # -m module [arg...] + # TODO: argv[0] should be the path to the module file not the dotted name + sys.argv = [cli.module, *cli.args] + runpy.run_module(cli.module, run_name="__main__") + elif cli.command: # -c "command" + sys.argv = ["-c", *cli.args] + exec(cli.command) + elif cli.interactive: # -i + run_interactive(cli.args) + elif cli.args: # script [arg...] import runpy - if args.command or args.interactive: - exclusive_error() - sys.argv = [args.module, *args.argv] - runpy.run_module(args.module, run_name="__main__") - elif args.command: # -c "command" - if args.argv or args.module or args.interactive: - exclusive_error() - exec(args.command) - elif args.interactive: # -i - if args.argv or args.module or args.command: - exclusive_error() - run_interactive() - else: + sys.argv = cli.args + runpy.run_path(cli.args[0], run_name="__main__") + else: # no arguments from sas.qtgui.MainWindow.MainWindow import run_sasview as run_gui + # sys.argv is unchanged run_gui() if __name__ == "__main__": From c007f6eedc2cb02a09a783b1e8758f8294395c49 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 15:51:50 -0400 Subject: [PATCH 10/28] Allow main to return a status code. --- installers/sasview.py | 2 +- run.py | 2 +- src/sas/cli.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/installers/sasview.py b/installers/sasview.py index fe5c14bdc4..7e8cfb4c49 100644 --- a/installers/sasview.py +++ b/installers/sasview.py @@ -12,4 +12,4 @@ from multiprocessing import freeze_support freeze_support() from sas.cli import main - main() + sys.exit(main()) diff --git a/run.py b/run.py index 74ae591855..c9cb5add8e 100644 --- a/run.py +++ b/run.py @@ -85,4 +85,4 @@ def prepare(): if __name__ == "__main__": prepare() import sas.cli - sas.cli.main(logging="development") + sys.exit(sas.cli.main(logging="development")) diff --git a/src/sas/cli.py b/src/sas/cli.py index 85005309f8..9e4759c786 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -126,5 +126,7 @@ def main(logging="production"): # sys.argv is unchanged run_gui() + return 0 + if __name__ == "__main__": - main() + sys.exit(main()) From 0ed828d8d3f3105398c647d7d7f6c25c47c5df37 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 16:04:32 -0400 Subject: [PATCH 11/28] remove no-longer-used system/env.py module --- src/sas/system/env.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 src/sas/system/env.py diff --git a/src/sas/system/env.py b/src/sas/system/env.py deleted file mode 100644 index c595d305d0..0000000000 --- a/src/sas/system/env.py +++ /dev/null @@ -1,35 +0,0 @@ -# ** DEPRECATED ** -""" Interface for environment variable access - -This is intended to handle any conversion from the environment variable string to more natural types. -""" -import os -import logging -from typing import Optional - -class Envrironment: - - logger = logging.getLogger(__name__) - - @property - def sas_opencl(self) -> Optional[str]: - """ - Get the value of the environment variable SAS_OPENCL, which specifies which OpenCL device - should be used. - """ - - if "SAS_OPENCL" in os.environ: - return os.environ.get("SAS_OPENCL", "none") - else: - return None - - - @sas_opencl.setter - def sas_opencl(self, value: Optional[str]): - """ - Set the value of the environment variable SAS_OPENCL - """ - - os.environ["SAS_OPENCL"] = "none" if value is None else value - -env = Envrironment() \ No newline at end of file From 4ce228a5113b2535b10877ae9f7a338faab306c0 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 16:17:18 -0400 Subject: [PATCH 12/28] Remove nag to fix import shadowing; it's unrelated to this PR --- src/sas/system/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sas/system/__init__.py b/src/sas/system/__init__.py index 4e42ad94fd..5b84646217 100644 --- a/src/sas/system/__init__.py +++ b/src/sas/system/__init__.py @@ -1,4 +1,3 @@ -# TODO: Don't shadow module names; it makes debugging difficult. from .web import web from .legal import legal from .env import env From 871915698a2c179204ed16335ab030a9a964ea0f Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 16:52:39 -0400 Subject: [PATCH 13/28] Move qt environment variable setup to sas.cli --- src/sas/cli.py | 1 + src/sas/qtgui/MainWindow/MainWindow.py | 8 +------- src/sas/system/__init__.py | 3 +-- src/sas/system/lib.py | 27 ++++++++++++++++++++++++-- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index 9e4759c786..3084867e94 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -103,6 +103,7 @@ def main(logging="production"): 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 diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index ca2ec2a846..e93469f393 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -76,13 +76,7 @@ def run_sasview(): import signal signal.signal(signal.SIGINT, signal.SIG_DFL) - # TODO: Move to sas.cli.main if needed for scripts. - # sas.cli.main does not yet import sas.config so leave here until needed. - # Alternative: define a qt setup function that can be called from a script. - os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" - os.environ["QT_SCALE_FACTOR"] = f"{config.QT_SCALE_FACTOR}" - os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" if config.QT_AUTO_SCREEN_SCALE_FACTOR else "0" - + # Note: Qt environment variables are initialized in sas.system.lib.setup_qt_env app = QApplication([]) # Main must have reference to the splash screen, so making it explicit diff --git a/src/sas/system/__init__.py b/src/sas/system/__init__.py index 5b84646217..34eca78358 100644 --- a/src/sas/system/__init__.py +++ b/src/sas/system/__init__.py @@ -1,6 +1,5 @@ from .web import web from .legal import legal -from .env import env from .config.config import config -__all__ = ["web", "legal", "env", "config"] \ No newline at end of file +__all__ = ["web", "legal", "config"] \ No newline at end of file diff --git a/src/sas/system/lib.py b/src/sas/system/lib.py index a12b4b787c..203da28470 100644 --- a/src/sas/system/lib.py +++ b/src/sas/system/lib.py @@ -1,9 +1,13 @@ -# Setup third-party libraries (e.g., sasview, periodictable, bumps) +""" +Setup third-party libraries (e.g., qt, sasview, periodictable, bumps) + +These functions are used to setup up the GUI and the scripting environment. +""" import os # TODO: Add api to control sasmodels rather than using environment variables def setup_sasmodels(): - """Initialize sasmodels settings""" + """Initialize sasmodels settings from the sasview configuration.""" from .user import get_user_dir # Don't need to set SAS_MODELPATH for gui because sascalc.fit uses the @@ -32,6 +36,10 @@ def setup_sasmodels(): config.SAS_OPENCL = SAS_OPENCL def reset_sasmodels(sas_opencl): + """ + Trigger a reload of all sasmodels calculators using the new value of + sas_opencl. The new value will be saved in the sasview configuration file. + """ from sasmodels.sasview_model import reset_environment from sas import config @@ -39,3 +47,18 @@ def reset_sasmodels(sas_opencl): os.environ["SAS_OPENCL"] = sas_opencl # CRUFT: next version of reset_environment() will return env reset_environment() + +def setup_qt_env(): + """ + Setup the Qt environment. + + The environment values are set by the user and managed by sasview config. + + This function does not import the Qt libraries so it is safe to use from + a script. + """ + from sas import config + + os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" + os.environ["QT_SCALE_FACTOR"] = f"{config.QT_SCALE_FACTOR}" + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" if config.QT_AUTO_SCREEN_SCALE_FACTOR else "0" From 61608961122e30a171ffe190c7dd00c82d095f53 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 17:47:59 -0400 Subject: [PATCH 14/28] redo command line interface with flags for -i/-q/-V --- src/sas/cli.py | 70 ++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index 3084867e94..172fa7cedd 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -1,20 +1,20 @@ """ SasView command line interface. -sasview -V - Print SasView version and exit. -sasview -m module [args...] +sasview [options] -m module [args...] Run module as main. -sasview -c "python statements" [args...] +sasview [options] -c "python statements" [args...] Execute python statements with sasview libraries available. -sasview -i [args...] - Start ipython interpreter using args. -sasview script [args...] +sasview [options] script [args...] Run script with sasview libraries available -sasview - Start sasview gui +sasview [options] + Start sasview gui if not interactive -You can also run it as "python -m sas.cli". +Options: + + -i: Start an interactive interpreter after the command is executed. + -q: Don't print. + -V: Print SasView version. """ import sys @@ -41,7 +41,9 @@ def parse_cli(argv): parser.add_argument("-c", "--command", type=str, help="Execute command") parser.add_argument("-i", "--interactive", action='store_true', - help="Start ipython with remaining arguments") + help="Start interactive interpreter after running command") + 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") @@ -63,9 +65,7 @@ def parse_cli(argv): # with the trigger (e.g., -mmodule) if have_trigger: collect_rest = True - elif arg in ('-i', '--interactive'): - have_trigger = collect_rest = True - elif arg.startswith('-m') or arg.startswith('--module'): + 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'): @@ -77,17 +77,6 @@ def parse_cli(argv): opts.args = rest return opts -def run_interactive(args): - """Run sasview as an interactive python interpreter""" - try: - from IPython import start_ipython - sys.argv = ["ipython", "--pylab", *args] - sys.exit(start_ipython()) - except ImportError: - import code - sys.argv = args - code.interact(local={'exit': sys.exit}) - def main(logging="production"): from sas.system import log from sas.system import lib @@ -108,24 +97,37 @@ def main(logging="production"): if cli.version: # -V import sas print(f"SasView {sas.__version__}") - elif cli.module: # -m module [arg...] + + 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] - runpy.run_module(cli.module, run_name="__main__") + context = runpy.run_module(cli.module, run_name="__main__") elif cli.command: # -c "command" sys.argv = ["-c", *cli.args] - exec(cli.command) - elif cli.interactive: # -i - run_interactive(cli.args) + exec(cli.command, context) elif cli.args: # script [arg...] import runpy sys.argv = cli.args - runpy.run_path(cli.args[0], run_name="__main__") - else: # no arguments - from sas.qtgui.MainWindow.MainWindow import run_sasview as run_gui + 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 - run_gui() + # Maybe hand cli.quiet to run_sasview? + run_sasview() + return 0 # don't drop into the interactive interpreter + + # TODO: Capture the global/local environment of the script/module + # 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 + exitmsg = banner = "" if cli.quiet else None + code.interact(banner=banner, exitmsg=exitmsg, local=context) return 0 From cf9fa6c8c394006fee7602c2524ca0b7a5321a6f Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 28 Oct 2022 18:04:02 -0400 Subject: [PATCH 15/28] pywin32 instead of win32 --- .github/workflows/installers.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- build_tools/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml index 7803ff89f1..577397b507 100644 --- a/.github/workflows/installers.yml +++ b/.github/workflows/installers.yml @@ -91,7 +91,7 @@ jobs: - name: Install pyopencl (Windows) if: ${{ matrix.os == 'windows-latest' }} run: | - python -m pip install pytools mako cffi win32 + python -m pip install pytools mako cffi pywin32 choco install opencl-intel-cpu-runtime choco install innosetup python -m pip install --only-binary=pyopencl --find-links http://www.silx.org/pub/wheelhouse/ --trusted-host www.silx.org pyopencl diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fb1d7b8a7..8adbf25e2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,7 +91,7 @@ jobs: - name: Install pyopencl (Windows) if: ${{ matrix.os == 'windows-latest' }} run: | - python -m pip install pytools mako cffi win32 + python -m pip install pytools mako cffi pywin32 choco install opencl-intel-cpu-runtime choco install innosetup python -m pip install --only-binary=pyopencl --find-links http://www.silx.org/pub/wheelhouse/ --trusted-host www.silx.org pyopencl diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7aabbb0a3..f622b37a56 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: - name: Install pyopencl (Windows) if: ${{ matrix.os == 'windows-latest' }} run: | - python -m pip install pytools mako cffi + python -m pip install pytools mako cffi pywin32 choco install opencl-intel-cpu-runtime python -m pip install --only-binary=pyopencl --find-links http://www.silx.org/pub/wheelhouse/ --trusted-host www.silx.org pyopencl diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt index f6b6fcaf67..3f3214acd4 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -34,4 +34,4 @@ importlib-resources bumps html2text jsonschema -win32; platform_system == "Windows" +pywin32; platform_system == "Windows" From 07c3efb01a735516e8d0b8568a2fb1ee59b8fd3f Mon Sep 17 00:00:00 2001 From: smk78 Date: Sat, 29 Oct 2022 11:56:45 +0100 Subject: [PATCH 16/28] Updated following changes to scripting.rst --- src/sas/cli.py | 51 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index 172fa7cedd..d3e3c6484d 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -1,20 +1,39 @@ +# +#Also see sasmodels\doc\guide\scripting.rst +# """ -SasView command line interface. - -sasview [options] -m module [args...] - Run module as main. -sasview [options] -c "python statements" [args...] - Execute python statements with sasview libraries available. -sasview [options] script [args...] - Run script with sasview libraries available -sasview [options] - Start sasview gui if not interactive - -Options: - - -i: Start an interactive interpreter after the command is executed. - -q: Don't print. - -V: Print SasView version. +**SasView command line interface** + +This functionality is under development. + +**Usage:** + +sasview [flags] + *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* + +**Flags:** + + -i: Interactive. *Start an interactive Python interpreter console.* + + -q: Quiet. *Suppress startup messages.* + + -V: Version. *Return the SasView version number.* + +Note: On Windows, any console output gets written to NUL by default. If +redirecting to STDOUT does not work, try redirecting (e.g. with >)/writing +output to file. + """ import sys From 999bd8d5c5755f1e4e62e1031fb596f8517d070f Mon Sep 17 00:00:00 2001 From: smk78 Date: Sun, 30 Oct 2022 21:15:52 +0000 Subject: [PATCH 17/28] Changes to make SasView CLI help available --- docs/sphinx-docs/source/user/working.rst | 4 ++++ src/sas/cli.py | 2 +- src/sas/qtgui/Perspectives/Fitting/media/cli.rst | 11 +++++++++++ src/sas/qtgui/Perspectives/Fitting/media/fitting.rst | 2 -- 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 src/sas/qtgui/Perspectives/Fitting/media/cli.rst diff --git a/docs/sphinx-docs/source/user/working.rst b/docs/sphinx-docs/source/user/working.rst index de41a337e2..3e247e9697 100644 --- a/docs/sphinx-docs/source/user/working.rst +++ b/docs/sphinx-docs/source/user/working.rst @@ -18,6 +18,10 @@ Working with SasView Writing a Plugin Model + Scripting Interface to sasmodels + + Command Line Interpreter Syntax + Environment Variables Model marketplace diff --git a/src/sas/cli.py b/src/sas/cli.py index d3e3c6484d..edd0f907e2 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -1,5 +1,5 @@ # -#Also see sasmodels\doc\guide\scripting.rst +#Also see sasview\src\sas\qtgui\Perspectives\Fitting\media\cli.rst # """ **SasView command line interface** diff --git a/src/sas/qtgui/Perspectives/Fitting/media/cli.rst b/src/sas/qtgui/Perspectives/Fitting/media/cli.rst new file mode 100644 index 0000000000..320bb605f2 --- /dev/null +++ b/src/sas/qtgui/Perspectives/Fitting/media/cli.rst @@ -0,0 +1,11 @@ +.. cli.rst + +.. This is a fudge to pick up the doc strings in cli.py which cannot be directly +.. included in a toctree because cli.py is not an rst file. Steve King, Oct 2022. + +Command Line Interpreter +======================== + +For details of the supported command syntax, see :mod:`sas.cli` . + +.. ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ diff --git a/src/sas/qtgui/Perspectives/Fitting/media/fitting.rst b/src/sas/qtgui/Perspectives/Fitting/media/fitting.rst index df9fc7463d..1423ba5b06 100755 --- a/src/sas/qtgui/Perspectives/Fitting/media/fitting.rst +++ b/src/sas/qtgui/Perspectives/Fitting/media/fitting.rst @@ -30,6 +30,4 @@ Fitting Documentation Computations with a GPU - Scripting Interface to sasmodels - References From 0bffa7832c18efbfd5ecdbc6c4f33f2a246b7045 Mon Sep 17 00:00:00 2001 From: smk78 Date: Mon, 31 Oct 2022 09:09:46 +0000 Subject: [PATCH 18/28] Add comment --- src/sas/qtgui/Perspectives/Fitting/media/cli.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/media/cli.rst b/src/sas/qtgui/Perspectives/Fitting/media/cli.rst index 320bb605f2..461cfa03ca 100644 --- a/src/sas/qtgui/Perspectives/Fitting/media/cli.rst +++ b/src/sas/qtgui/Perspectives/Fitting/media/cli.rst @@ -1,7 +1,10 @@ .. cli.rst -.. This is a fudge to pick up the doc strings in cli.py which cannot be directly -.. included in a toctree because cli.py is not an rst file. Steve King, Oct 2022. +.. This is a fudge to pick up the doc strings in sasview\src\sas\cli.py which +.. cannot be directly included in a toctree because cli.py is not an rst file. +.. You also cannot link this module from Sasmodels because it doesn't know about +.. cli.py and will generate a Sasmodels doc build error if you do! +.. Steve King, Oct 2022. Command Line Interpreter ======================== From d0d64140a2a42f9359b3ecd2c0dd5da91a843602 Mon Sep 17 00:00:00 2001 From: smk78 Date: Mon, 31 Oct 2022 09:12:03 +0000 Subject: [PATCH 19/28] Text furtling --- docs/sphinx-docs/source/user/working.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sphinx-docs/source/user/working.rst b/docs/sphinx-docs/source/user/working.rst index 3e247e9697..2da9c0c4e0 100644 --- a/docs/sphinx-docs/source/user/working.rst +++ b/docs/sphinx-docs/source/user/working.rst @@ -18,12 +18,12 @@ Working with SasView Writing a Plugin Model - Scripting Interface to sasmodels + Scripting Interface to Sasmodels Command Line Interpreter Syntax Environment Variables - Model marketplace + Model Marketplace Preferences From ace822c0e503a75f71c7ccb7712f7f1f98a0ba3c Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Mon, 31 Oct 2022 14:12:47 -0400 Subject: [PATCH 20/28] tweak help text; exit immediately after version --- src/sas/cli.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index edd0f907e2..4945187da0 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -4,7 +4,8 @@ """ **SasView command line interface** -This functionality is under development. +This functionality is under development. Interactive sessions do not yet +work in the Windows console. **Usage:** @@ -22,18 +23,18 @@ sasview [flags] -c "python statements" [args...] *Execute python statements using the installed SasView libraries* -**Flags:** - - -i: Interactive. *Start an interactive Python interpreter console.* +sasview -V + *Print sasview version and exit.* - -q: Quiet. *Suppress startup messages.* +**Flags:** - -V: Version. *Return the SasView version number.* + -i, --interactive. *Enter an interactive session after command/module/script.* -Note: On Windows, any console output gets written to NUL by default. If -redirecting to STDOUT does not work, try redirecting (e.g. with >)/writing -output to file. + -q, --quiet. *Suppress startup messages on interactive console.* +Note: On Windows any console output gets written to NUL by default. If +output is not appearing then try redirecting to a file using for example +*sasview ... > output.txt*. """ import sys @@ -66,7 +67,7 @@ def parse_cli(argv): parser.add_argument("args", nargs="*", help="script followed by args") - # Special case: abort argument processing after -i, -m or -c. + # Special case: abort argument processing after -m or -c. have_trigger = False collect_rest = False keep = [] @@ -116,6 +117,8 @@ def main(logging="production"): 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...] @@ -137,7 +140,6 @@ def main(logging="production"): run_sasview() return 0 # don't drop into the interactive interpreter - # TODO: Capture the global/local environment of the script/module # TODO: Start interactive with ipython rather than normal python # For ipython use: # from IPython import start_ipython From ddae208abb9aa9217ce6b3c5f0d18f49f38f686d Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Mon, 31 Oct 2022 14:37:10 -0400 Subject: [PATCH 21/28] include sasview version in the interactive startup banner --- src/sas/cli.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index 4945187da0..4411d7cd43 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -147,7 +147,22 @@ def main(logging="production"): # sys.exit(start_ipython()) if cli.interactive: import code - exitmsg = banner = "" if cli.quiet else None + # 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 From a4e4aa2838d145bdbe07052c889025b77450d3f9 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Thu, 10 Nov 2022 13:18:32 -0500 Subject: [PATCH 22/28] minor tweaks from code review with no functional effect --- .github/workflows/ci.yml | 2 +- installers/sasview.spec | 1 - run.py | 8 ++++---- src/sas/qtgui/MainWindow/MainWindow.py | 2 +- src/sas/system/__init__.py | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 698dd6a993..e4566174fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: python -m pip install matplotlib~=3.5.2 python -m pip install lxml - - name: Install pywin32 (Windows + Linux) + - name: Install pywin32 (Windows) if: ${{ startsWith(matrix.os, 'windows') }} run: | python -m pip install pywin32 diff --git a/installers/sasview.spec b/installers/sasview.spec index 8c62b2f2fb..29e998ba8d 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -1,7 +1,6 @@ # -*- mode: python ; coding: utf-8 -*- import sys -import os from pathlib import Path import warnings import platform diff --git a/run.py b/run.py index c9cb5add8e..e85f9e6782 100644 --- a/run.py +++ b/run.py @@ -54,14 +54,14 @@ def prepare(): # Find the directories for the source and build root = abspath(dirname(realpath(__file__))) - # TODO: Do we prioritize the sister repo or the installed package? + # TODO: Do we prioritize the sibling repo or the installed package? # TODO: Can we use sasview/run.py from a distributed sasview.exe? # Put supporting packages on the path if they are not already available. - for sister in ('periodictable', 'bumps', 'sasdata', 'sasmodels'): + for sibling in ('periodictable', 'bumps', 'sasdata', 'sasmodels'): try: - import_module(sister) + import_module(sibling) except: - addpath(joinpath(root, '..', sister)) + addpath(joinpath(root, '..', sibling)) # Put the source trees on the path addpath(joinpath(root, 'src')) diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index f95c563dbf..d5c1b41a67 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -1,8 +1,8 @@ # UNLESS EXEPTIONALLY REQUIRED TRY TO AVOID IMPORTING ANY MODULES HERE # ESPECIALLY ANYTHING IN SAS, SASMODELS NAMESPACE +import logging import os import sys -import logging from sas.system.version import __version__ from sas.system import config diff --git a/src/sas/system/__init__.py b/src/sas/system/__init__.py index 34eca78358..38ea144944 100644 --- a/src/sas/system/__init__.py +++ b/src/sas/system/__init__.py @@ -2,4 +2,4 @@ from .legal import legal from .config.config import config -__all__ = ["web", "legal", "config"] \ No newline at end of file +__all__ = ["web", "legal", "config"] From ca6864e7c9af666ec0d9015aad8b7518db286bc6 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Thu, 23 Feb 2023 17:19:39 -0500 Subject: [PATCH 23/28] ticket 2237: undo merge breakage? --- .github/workflows/ci.yml | 5 ++++ src/sas/qtgui/MainWindow/MainWindow.py | 36 ++------------------------ 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcb3dc2378..1400fd7eb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,11 @@ jobs: python -m pip install wheel setuptools python -m pip install -r build_tools/requirements.txt + - name: Install pywin32 (Windows) + if: ${{ startsWith(matrix.os, 'windows') }} + run: | + python -m pip install pywin32 + - name: Install pyopencl (Windows) if: ${{ startsWith(matrix.os, 'windows') }} run: | diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index 6e868f2aaf..b6ef23cb57 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -5,7 +5,6 @@ import sys from sas.system.version import __version__ -from sas.system import config from PyQt5.QtWidgets import QMainWindow from PyQt5.QtWidgets import QMdiArea @@ -74,45 +73,14 @@ def get_highdpi_scaling(): def run_sasview(): - os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" - os.environ["QT_SCALE_FACTOR"] = f"{config.QT_SCALE_FACTOR}" - os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" if config.QT_AUTO_SCREEN_SCALE_FACTOR else "0" - - - app = QApplication([]) - - - app.setAttribute(Qt.AA_ShareOpenGLContexts) - - - #Initialize logger - from sas.system.log import SetupLogger - SetupLogger(__name__).config_development() - - # initialize sasmodels settings - from sas.system.user import get_user_dir - if "SAS_DLL_PATH" not in os.environ: - os.environ["SAS_DLL_PATH"] = os.path.join( - get_user_dir(), "compiled_models") - - # Set open cl config from environment variable, if it is set - - if env.sas_opencl is not None: - logging.getLogger(__name__).info("Getting OpenCL settings from environment variables") - config.SAS_OPENCL = env.sas_opencl - else: - logging.getLogger(__name__).info("Getting OpenCL settings from config") - env.sas_opencl = config.SAS_OPENCL - # Make the event loop interruptable quickly import signal signal.signal(signal.SIGINT, signal.SIG_DFL) # Note: Qt environment variables are initialized in sas.system.lib.setup_qt_env - app = QApplication([]) - # Main must have reference to the splash screen, so making it explicit - + app = QApplication([]) + app.setAttribute(Qt.AA_ShareOpenGLContexts) app.setAttribute(Qt.AA_EnableHighDpiScaling) app.setStyleSheet("* {font-size: 11pt;}") From c848247dd7387e9db747678bf8d813bbeccfd899 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 24 Feb 2023 20:16:27 -0500 Subject: [PATCH 24/28] 2237: Add I/O redirection for cli to sasview.exe --- src/sas/cli.py | 4 + src/sas/system/console.py | 176 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 src/sas/system/console.py diff --git a/src/sas/cli.py b/src/sas/cli.py index 4411d7cd43..e113de91df 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -100,6 +100,10 @@ def parse_cli(argv): def main(logging="production"): 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. + console.setup_console() # Eventually argument processing might affect logger or config, so do it first cli = parse_cli(sys.argv) diff --git a/src/sas/system/console.py b/src/sas/system/console.py new file mode 100644 index 0000000000..5560a03c2d --- /dev/null +++ b/src/sas/system/console.py @@ -0,0 +1,176 @@ +""" +Windows console binding for SasView +""" +import os, sys +import atexit + +def attach_windows_console(): + """ + Attach a console to a windows program that does not normally have one. + + Note: Uses a lazy import for win32console so you will to add pywin32 + to requirements.txt and tell the installer to include win32.win32console + """ + # Lazy import so we don't have to check for OS. + from win32 import win32console + if win32console.GetConsoleWindow() == 0: # No console attached + # The following kinda works but has flaky interaction with the existing prompt + #win32console.AttachConsole(-1) # Attach to parent console + # Instead create a new console for I/O and call it the sasview console + win32console.AllocConsole() + win32console.SetConsoleTitle('SasView console') + +class Singleton(type): + """ + Metaclass indicating that all object instantiations should return the same instance. + + Usage: + + class Stateful(metaclass=Singleton): ... + + The init will only be triggered for the first instance, so you probably shouldn't + parameterize it, or only parameterize it during setup before any other instances + are created. + """ + # Adam Forsyth (2011) + # https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python/6798042#6798042 + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + +class WindowsConsole(metaclass=Singleton): + """ + Windows console object. + + This only creates the console when you read to it or write from it. This reduces + flashing windows when using the app in a pipeline. + + This presents as an incomplete standard rw file interface. + + Unfortunately it does not regiister as a windows console stdio object so the + cpython myreadline code does not call PyOS_InputHook during read. The practical + consequence is that pyplot.ion() does not create an interactive plot, and you + instead need to call pyplot.pause(0.1) to draw the figure. You can try tracing + through myreadline.c to see what we need to do to get sys.stdin recognized as + stdin here: https://github.com/python/cpython/blob/main/Parser/myreadline.c + """ + def __init__(self): + self._attached = False + self._conin = None + self._conout = None + def close_wait(self): + """ + Registered with atexit to give users a chance to see the output. + """ + if self._conout is not None: + # An output console was opened for writing, so pause + self.write("Press enter to exit...") + self.flush() + self.readline() + def _attach_console(self): + if not self._attached: + attach_windows_console() + self._attached = True + @property + def _read_fd(self): + if self._conin is None: + self._attach_console() + self._conin = open("CONIN$", "r") + return self._conin + @property + def _write_fd(self): + if self._conout is None: + self._attach_console() + self._conout = open("CONOUT$", "w") + #self._conout.write("registering atexit...\n") + atexit.register(self.close_wait) + return self._conout + def readline(self, *args, **kwargs): + return self._read_fd.readline(*args, **kwargs) + def write(self, *args, **kwargs): + return self._write_fd.write(*args, **kwargs) + def flush(self): + return self._write_fd.flush() + # Unused + def read(self, *args, **kwargs): + return self._read_fd.read(*args, **kwargs) + def isatty(self): + return True + def readable(self): + return True + def writeable(self): + return True + def seekable(self): + return False + def name(self): + return "" + # Not implemented: + # buffer, close, closed, detach, encoding, errors, fileno, line_buffering, mode, + # newlines, readlines, reconfigure, seek, tell, truncate, write_through, write_lines + # __enter__, __exit__, __iter__, __next__ + +def setup_console(stderr_as="console"): + """ + Lazy redirect of stdio to windows console. + + Handling of stderr is defined by the caller: + + * console: create a console for stderr even if stdin/stdout are redirected. + * stdout: redirect stderr to whereever stdout is going + * null: redirect stderr to the NUL device (untested!!) + * none: don't redirect stderr; instead windows displays an error box with stderr contents + """ + if os.name == 'nt': # Make sure we are attached to a console + if sys.__stdin__ is None: + sys.__stdin__ = sys.stdin = WindowsConsole() + if sys.__stdout__ is None: + sys.__stdout__ = sys.stdout = WindowsConsole() + if sys.__stderr__ is None: + if stderr_as == "console": + stderr = WindowsConsole() + elif stderr_as == "stdout": + stderr = sys.__stdout__ + elif stderr_as == "null": + # TODO: Untested !! + stderr = open("NUL:", "w") + elif stderr_as == "none": + stderr = None + sys.__stderr__ = sys.stderr = stderr + +def setup_console_simple(stderr_to_stdout=True): + """ + Simple version of stdio redirection: always open a console, and don't pause before closing. + """ + if os.name == 'nt': + def console_open(mode): + attach_windows_console() + return open("CON:", "r") if mode == "r" else open("CON:", "w") + if sys.__stdin__ is None: + sys.__stdin__ = sys.stdin = console_open("r") + if sys.__stdout__ is None: + sys.__stdout__ = sys.stdout = console_open("w") + if sys.__stderr__ is None: + sys.__stderr__ = sys.stderr = sys.__stdout__ if stderr_to_stdout else console_open("w") + sys.__stderr__ = sys.stderr = console_open("w") + + +def demo(): + setup_console() + if 0: + import win32 + from win32 import win32console + from win32 import win32gui + from win32 import win32process, win32api + pid = win32process.GetWindowThreadProcessId(hwnd) + handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ, False, pid[1]) + proc_name = win32process.GetModuleFileNameEx(handle, 0) + print(proc_name) + print("demo ready") + import code; code.interact(local={'exit': sys.exit}) + print('demo done') + #import time; time.sleep(2) + +if __name__ == "__main__": + demo() From 7e201cfb5e111856453f52b082bfa7d2d48c9f2e Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 24 Feb 2023 21:07:07 -0500 Subject: [PATCH 25/28] 2237: Disable console redirect. --- src/sas/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index e113de91df..8064db277e 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -102,8 +102,8 @@ def main(logging="production"): from sas.system import lib from sas.system import console - # I/O redirection for the windows console. Need to do this early. - console.setup_console() + ## I/O redirection for the windows console. Need to do this early. + #console.setup_console() # Eventually argument processing might affect logger or config, so do it first cli = parse_cli(sys.argv) From f6b25a779e900c2acec76608a8e0122f0d546782 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 28 Feb 2023 16:16:31 -0500 Subject: [PATCH 26/28] 2237: Option to open console on startup. --- src/sas/cli.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index 8064db277e..610bf71d3d 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -102,8 +102,14 @@ def main(logging="production"): from sas.system import lib from sas.system import console - ## I/O redirection for the windows console. Need to do this early. - #console.setup_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 len(sys.argv) > 1 and sys.argv[1] == "con": + console.setup_console() + sys.argv = [sys.argv[0], *sys.argv[1:]] # Eventually argument processing might affect logger or config, so do it first cli = parse_cli(sys.argv) From fce99aef985cfa7d975232e01a2ced5b583fb4b5 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 28 Feb 2023 16:38:26 -0500 Subject: [PATCH 27/28] 2237: Change option to open console on startup. --- src/sas/cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index 610bf71d3d..d34b5d7d74 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -62,6 +62,8 @@ def parse_cli(argv): 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="*", @@ -107,9 +109,8 @@ def main(logging="production"): # 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 len(sys.argv) > 1 and sys.argv[1] == "con": + if "-i" in sys.argv[1:] or "-o" in sys.argv[1:]: console.setup_console() - sys.argv = [sys.argv[0], *sys.argv[1:]] # Eventually argument processing might affect logger or config, so do it first cli = parse_cli(sys.argv) From efe6564817071fcc06feb2711f31236726b27479 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 7 Mar 2023 10:39:06 -0500 Subject: [PATCH 28/28] 2237: Document console output option for windows. --- src/sas/cli.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/sas/cli.py b/src/sas/cli.py index d34b5d7d74..e1ef60fa6e 100644 --- a/src/sas/cli.py +++ b/src/sas/cli.py @@ -30,11 +30,13 @@ -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 gets written to NUL by default. If -output is not appearing then try redirecting to a file using for example -*sasview ... > output.txt*. +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