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/build_tools/requirements.txt b/build_tools/requirements.txt index 416a3c54f7..584b60e63e 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -35,6 +35,7 @@ importlib-resources bumps html2text jsonschema +pywin32; platform_system == "Windows" superqt pyopengl pyopengl_accelerate diff --git a/docs/sphinx-docs/source/user/working.rst b/docs/sphinx-docs/source/user/working.rst index de41a337e2..2da9c0c4e0 100644 --- a/docs/sphinx-docs/source/user/working.rst +++ b/docs/sphinx-docs/source/user/working.rst @@ -18,8 +18,12 @@ Working with SasView Writing a Plugin Model + Scripting Interface to Sasmodels + + Command Line Interpreter Syntax + Environment Variables - Model marketplace + Model Marketplace Preferences diff --git a/installers/sasview.py b/installers/sasview.py index 9e3ad44ef9..7e8cfb4c49 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 + sys.exit(main()) diff --git a/installers/sasview.spec b/installers/sasview.spec index 7f7c43a0d0..fca9705b5f 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -65,6 +65,13 @@ hiddenimports = [ 'uncertainties', ] +if platform.system() == 'Windows': + # Need win32 to run sasview from the command line. + hiddenimports.extend([ + 'win32', + 'win32.win32console', + ]) + a = Analysis( ['sasview.py'], pathex=[], diff --git a/run.py b/run.py index 4fd65f4c53..e85f9e6782 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 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 sibling in ('periodictable', 'bumps', 'sasdata', 'sasmodels'): + try: + import_module(sibling) + except: + addpath(joinpath(root, '..', sibling)) # 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 + sys.exit(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..e1ef60fa6e --- /dev/null +++ b/src/sas/cli.py @@ -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] + *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"): + 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) + 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__": + sys.exit(main()) diff --git a/src/sas/qtgui/MainWindow/MainWindow.py b/src/sas/qtgui/MainWindow/MainWindow.py index 9d9a26cbe5..b6ef23cb57 100644 --- a/src/sas/qtgui/MainWindow/MainWindow.py +++ b/src/sas/qtgui/MainWindow/MainWindow.py @@ -4,8 +4,7 @@ import os import sys -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 @@ -18,6 +17,8 @@ from sas.qtgui.UI import main_resources_rc 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): @@ -26,7 +27,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. @@ -46,8 +47,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): @@ -73,42 +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 # 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;}") diff --git a/src/sas/qtgui/Perspectives/Fitting/GPUOptions.py b/src/sas/qtgui/Perspectives/Fitting/GPUOptions.py index fb98cca687..cc844c7c1c 100644 --- a/src/sas/qtgui/Perspectives/Fitting/GPUOptions.py +++ b/src/sas/qtgui/Perspectives/Fitting/GPUOptions.py @@ -19,8 +19,8 @@ from sas.qtgui.Perspectives.Fitting.UI.GPUTestResultsUI import Ui_GPUTestResults from sas.qtgui.Utilities.Preferences.PreferencesWidget import PreferencesWidget -from sas.system import env from sas import config +from sas.system import lib try: _fromUtf8 = QtCore.QString.fromUtf8 @@ -119,11 +119,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/qtgui/Perspectives/Fitting/media/cli.rst b/src/sas/qtgui/Perspectives/Fitting/media/cli.rst new file mode 100644 index 0000000000..461cfa03ca --- /dev/null +++ b/src/sas/qtgui/Perspectives/Fitting/media/cli.rst @@ -0,0 +1,14 @@ +.. cli.rst + +.. 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 +======================== + +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 diff --git a/src/sas/system/__init__.py b/src/sas/system/__init__.py index 5b84646217..38ea144944 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"] 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() diff --git a/src/sas/system/env.py b/src/sas/system/env.py deleted file mode 100644 index 35da0511ba..0000000000 --- a/src/sas/system/env.py +++ /dev/null @@ -1,34 +0,0 @@ -""" 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 diff --git a/src/sas/system/lib.py b/src/sas/system/lib.py new file mode 100644 index 0000000000..203da28470 --- /dev/null +++ b/src/sas/system/lib.py @@ -0,0 +1,64 @@ +""" +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 from the sasview configuration.""" + 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): + """ + 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 + + config.SAS_OPENCL = 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" diff --git a/src/sas/system/log.py b/src/sas/system/log.py index 87813b2664..e5e06dfd90 100644 --- a/src/sas/system/log.py +++ b/src/sas/system/log.py @@ -73,3 +73,9 @@ def _find_config_file(self, filename="log.ini"): self._config_file = importlib.resources.open_text('sas.system', filename) except FileNotFoundError: print(f"ERROR: '{filename}' not found...", file=sys.stderr) + +def production(): + return SetupLogger("sasview").config_production() + +def development(): + return SetupLogger("sasview").config_development()