Source code for _main

#
##
##  SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.5  (Thu Feb  8 19:11:13 CET 2024)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##
"""pyFormex main module

This module contains the main function of pyFormex, which is normally run
by the ``pyformex`` command.
"""
import sys
import os
import code

from contextlib import redirect_stdout, redirect_stderr

import pyformex as pf

on_exit = []  # functions to run on exit

[docs]def onExit(func): """Register a function to be called on exit from pyFormex""" if callable(func): on_exit.append(func) else: raise ValueError("Expected a callable")
[docs]def load_user_config(): # noqa: C901 """Load the pyFormex configuration Notes ----- This function should be called to create a proper configuration when pyFormex is imported in Python and not started from the pyformex command. """ # pf.logger.info("Loading configuration files") from pyformex import Path from pyformex.config import Config # Set the config files if pf.options.nodefaultconfig: sysprefs = [] userprefs = [] else: sysprefs = [pf.cfg['siteprefs']] newprefs = pf.cfg['userprefs'] if not newprefs.exists(): # try copying from old prefs oldprefs = pf.cfg['userconfdir'] / 'pyformex.conf' oldprefs.copy(newprefs) userprefs = [newprefs] if pf.cfg['localprefs'].exists(): userprefs.append(pf.cfg['localprefs']) if pf.options.config: userprefs.append(Path(pf.options.config).expanduser()) if len(userprefs) == 0: # We should always have a place to store the user preferences userprefs = [pf.cfg['userprefs']] # Use last one to save preferences if pf.debugon(pf.DEBUG.CONFIG): print(f"System Preference Files: {sysprefs}") if pf.debugon(pf.DEBUG.CONFIG): print(f"User Preference Files: {userprefs}") pf.preffile = Path.resolve(userprefs.pop()) # Read sysprefs as reference for f in sysprefs: if f.exists(): if pf.debugon(pf.DEBUG.CONFIG): print(f"Reading config file {f}") pf.cfg.load(f) # Set this as reference config pf.refcfg = pf.cfg if pf.debugon(pf.DEBUG.CONFIG): print("=" * 60) if pf.debugon(pf.DEBUG.CONFIG): print(f"RefConfig: {pf.refcfg}") pf.cfg = Config(default=pf.refcfg) # Read userprefs as reference for f in userprefs: if f.exists(): if pf.debugon(pf.DEBUG.CONFIG): print(f"Reading config file {f}") pf.cfg.load(f) else: if pf.debugon(pf.DEBUG.CONFIG): print(f"Skip non-existing config file {f}") if pf.preffile.exists(): if pf.debugon(pf.DEBUG.CONFIG): print(f"Reading config file {pf.preffile}") pf.cfg.load(pf.preffile) else: # Create the config file if pf.debugon(pf.DEBUG.CONFIG): print(f"Creating config file {pf.preffile}") try: Path.mkdir(pf.preffile.parent, parents=True, exist_ok=True) pf.preffile.touch() except Exception: pf._startup.messages.append( f"Could not create the user configuration file {pf.preffile}.\n" "User preferences will not be saved.\n") pf.preffile = None # Set this as preferences config pf.prefcfg = pf.cfg # TODO: FIX THIS CONCEPTUALLY # Make sure that we have separate warnings filters if id(pf.prefcfg['warnings/filters']) == id(pf.refcfg['warnings/filters']): pf.prefcfg['warnings/filters'] = set() if pf.debugon(pf.DEBUG.CONFIG): print("=" * 60) print(f"Config: {pf.prefcfg}") pf.cfg = Config(default=pf.prefcfg) # Fix incompatible changes in configuration _sanitize_config(pf.prefcfg) # Make sure we have a writeable tmpdir if not pf.cfg['tmpdir'].is_writable_dir(): tmpdir = Path(os.environ.get('TMPDIR', pf.cfg['homedir'])) if tmpdir.is_writable_dir(): tmpdir = tmpdir / 'pyformex_tmp' tmpdir.mkdir(parents=True, exist_ok=False) if tmpdir.is_writable_dir(): pf.cfg['tmpdir'] = tmpdir return raise ValueError( "I could not find a writable path for temporary files." " You can specify one with the --tmpdir option of the" " pyformex command.")
def _sanitize_warnings_filters(filters): """Sanitize the 'warnings/filters' setting. The setting should be a set (to avoid doubles), have tuples of length 4, not contain 'D' categories and category should be 'P' for pyFormex messages. Returns the corrected setting, possibly None. """ if pf.debugon(pf.DEBUG.WARNING): print('Sanitizing warning filters', filters) from pyformex.messages import _messages if not isinstance(filters, (set, list)): return None okfilters = set() for f in filters: if not isinstance(f, tuple) or len(f) != 4: continue if f[1] == '': # Old format message, module, c, a = f if c == 'D': continue else: a, message, c, module = f message = message.rstrip('$') if c == 'U' and message in _messages: c = 'P' okfilters.add((a, message, c, module if module else '')) if pf.debugon(pf.DEBUG.WARNING): print("Sanitized warnings filters configuration:", okfilters) return okfilters def _sanitize_config(cfg): # noqa: C901 """Apply incompatible changes in the configuration cfg is the user configuration that is to be saved. """ from pyformex import Path if cfg == pf.refcfg: # refcfg is the reference config return if pf.debugon(pf.DEBUG.CONFIG): print('Sanitizing settings') if pf.debugon(pf.DEBUG.WARNING): c = _sanitize_warnings_filters(cfg['warnings/filters']) cfg['warnings/filters'] = c # Path required for p in ('workdir', ): if p in cfg and not isinstance(cfg[p], Path): cfg[p] = Path(p) # Adhoc changes if not isinstance(cfg.get('gui/redirect', ''), str): del cfg['gui/redirect'] if isinstance(cfg['gui/dynazoom'], str): cfg['gui/dynazoom'] = [cfg['gui/dynazoom'], ''] for i in range(8): t = f"render/light{i}" try: cfg[t] = dict(cfg[t]) except Exception: pass for d in ['scriptdirs', 'appdirs']: if d in cfg: scriptdirs = [] for i in cfg[d]: if i[1] == '' or Path(i[1]).is_dir(): scriptdirs.append(tuple(i)) elif i[0] == '' or Path(i[0]).is_dir(): scriptdirs.append((i[1], i[0])) cfg[d] = scriptdirs # Rename settings for old, new in ( ('history', 'gui/scripthistory'), ('gui/history', 'gui/scripthistory'), ('raiseapploadexc', 'showapploaderrors'), ('webgl/xtkscript', 'webgl/script'), ): if old in cfg: if new not in cfg: cfg[new] = cfg[old] del cfg[old] # Delete keys that are not in refcfg delkeys = set(cfg.keys()) - set(pf.refcfg.keys()) # use keys()!! if delkeys: print("DELETING CONFIG VARIABLES:", delkeys) for key in delkeys: del cfg[key] # Remove invalid menus # if 'gui/menus' in cfg: # cfg['gui/menus'] = [m for m in cfg['gui/menus'] # if m in pf.refcfg['gui/menus']]
[docs]def savePreferences(): """Save the preferences. The name of the preferences file is determined at startup from the configuration files, and saved in ``pyformex.preffile``. If a local preferences file was read, it will be saved there. Otherwise, it will be saved as the user preferences, possibly creating that file. If ``pyformex.preffile`` is None, preferences are not saved. """ if pf.debugon(pf.DEBUG.CONFIG | pf.DEBUG.INFO): print(f"Saving preferences to: {pf.preffile}") if pf.preffile is None: return # Create the user conf dir pf.preffile.parent.mkdir(parents=True, exist_ok=True) # Do not store the refcfg warning filters: we add them on startup pf.prefcfg['warnings/filters'] -= pf.refcfg['warnings/filters'] # Currently erroroneously processed, therefore not saved del pf.prefcfg['render/light0'] del pf.prefcfg['render/light1'] del pf.prefcfg['render/light2'] del pf.prefcfg['render/light3'] if pf.GUI: # in case we get called from Settings menu: update this pf.GUI.writeSettings() if pf.debugon(pf.DEBUG.CONFIG): print("=" * 60) if pf.debugon(pf.DEBUG.CONFIG): print(f"!!!Saving config:\n{pf.prefcfg}") try: pf.prefcfg.write(pf.preffile) if pf.debugon(pf.DEBUG.CONFIG): print(f"Saved preferences to file {pf.preffile}") return True except Exception as e: print(e) print("!!! The user preferences were not saved!!!") return False
[docs]def override_config(option, setting, delete=True): """Override a config setting with a command line option Parameters ---------- option: str Name of the option that should override a setting. setting: str Key of the setting to be overridden. delete: bool If True (Default), delete the option after use Notes ----- If the option was given, its value is written into the corresponding setting and the option is deleted. """ value = getattr(pf.options, option) if value is not None and value != pf.cfg[setting]: if pf.debugon(pf.DEBUG.CONFIG): print(f"Override config value '{setting}={pf.cfg[setting]}" f" with value '{value}'") pf.cfg[setting] = value if delete: delattr(pf.options, option) # avoid abuse
def split_args(args): opts = [a for a in args if a.startswith('-')] args = [a for a in args if a not in opts] extended = '-a' in opts if extended: opts.remove('-a') return opts, args, extended ########################### main ################################
[docs]def main(args=[]): # noqa: C901 """The pyFormex main function. This function is normally executed by the ``pyformex`` launcher script, but it can also be run from a Python interpreter where the pyformex module has been imported. Parameters ---------- args: list | str The list of pyformex command line options to be used. Run the command ``pyformex --help`` to get the full list. Returns ------- int: The returncode. The value depends on what operations the run function executed and how it ended. Usually a value 0 is returned, meaning that the operation terminated normally. The pyFormex launcher script returns with this value to the operating system. Notes ----- After pyFormex launcher script has correctly set up the Python import paths, this function is executed. It is responsible for reading the configuration file(s), processing the command line options and starting the application. The basic configuration file is 'pyformexrc' located in the pyFormex main directory. It should always be present and be left unchanged. If you want to make changes, copy (parts of) this file to another location where you can change them. Then make sure pyFormex reads you modifications file. By default, pyFormex will try to read the following configuration files if they are present (and in this order):: default settings: <pyformexdir>/pyformexrc (always loaded) system-wide settings: /etc/pyformex.conf user settings: <configdir>/pyformex/pyformex.conf local settings $PWD/.pyformexrc Also, an extra config file can be specified in the command line, using the --config option. The system-wide and user settings can be skipped by using the --nodefaultconfig option. Config files are loaded in the above order. Settings always override those loaded from a previous file. When pyFormex exits, the preferences that were changed are written to the last read config file. Changed settings are those that differ from the settings obtained after loading all but the last config file. If none of the optional config files exists, a new user settings file will be created, and an error will occur if the <configdir> path is not writable. """ if isinstance(args, (str, bytes)): import shlex args = shlex.split(args) elif not isinstance(args, list): raise pf.ImplementationError(f"invalid args {args=}") args = sys.argv[1:] # Parse the command line options (and process simple options) from pyformex.parser import parseOptions options = parseOptions(args) if options is None: return 0 # Make options globally available pf.options = options # When we get here, we only have loaded: pyformex, pyformex.config, # pyformex.main, pyformex.mydict, pyformex.options, pyformex.path from pyformex import utils pf.warning = utils.warning pf.error = utils.error # Process the cmdtools options (depend upon pf.options being set) # print("BEFORE", pf.options) from pyformex.cmdtools import processOptions if processOptions(pf.options) is not None: return 0 ## Load the user configuration ## load_user_config() ## Process special options which do not start pyFormex ## but depend on the user configuration if pf.options.search or pf.options.listfiles: opts, args, extended = split_args(pf.options.args) if pf.options.search: search = args.pop(0) if len(args) > 0: files = args else: files = utils.sourceFiles(relative=True, extended=extended) if pf.options.listfiles: print('\n'.join(files)) else: if "'" in search: search.replace("'", "\'") print(f"SEARCH = [{search}]", file=sys.stderr) options = ' '.join(opts) quotedfiles = ' '.join([f"'{f}'" for f in files]) cmd = f'grep {options} "{search}" {quotedfiles}' os.system(cmd) return 0 ## If we get here, we want to start pyFormex ## Process options that override the config ## # Config settings that are overridden by the matching option for option, setting in ( ('bindings', 'gui/bindings'), ('redirect', 'gui/redirect'), ('opengl', 'opengl/version'), ): override_config(option, setting) utils.setSaneLocale() ## Initialize numpy ## # Note: this will use user settings pf._startup.initialize_numpy(version=pf._startup.numpy_version, printoptions=pf.cfg['numpy/printoptions'], typelessdata=pf.cfg['numpy/typelessdata']) # TODO: with gui: # Minimal pyside2/pyqt5 is 5.11; recommended is 5.15 # Initialize the libraries if pf.options.uselib is None: pf.options.uselib = pf.cfg['uselib'] # Force initialisation of the library from pyformex import lib # noqa: F401 # Import core pyformex language into pf : DO THIS AFTER LIB ! pf._import_core() ## Activate the warning filters utils.resetWarningFilters() if pf.cfg['warnings/nice']: utils.set_warnings(nice=True, popup=False) # Make sure pf.PF is a Project from pyformex.project import Project pf.PF = Project() pf.PM = None # Add a configured syspath if pf.cfg['syspath']: sys.path.extend(pf.cfg['syspath']) # print("SYSPATH", sys.path) # Set application paths if pf.debugon(pf.DEBUG.INFO): print("Loading AppDirs") from pyformex import apps apps.setAppDirs() # print("SYSPATH2", sys.path) args = pf.options.args # check if we have batch tasks (inline script or script file) batch = (pf.options.script is not None or (len(args) > 0 and utils.is_script(args[0]))) if pf.options.gui is None: # set nogui if batch, else gui pf.options.gui = not batch if not pf.options.gui and not batch: # nogui without batch task: go interactive pf.options.interactive = True # Create the interpreter for script mode context = {'pf': pf} pf.interpreter = code.InteractiveInterpreter(context) onExit(savePreferences) stdout = sys.stdout stderr = sys.stderr # Start the GUI if needed (should be done after the config is set) if pf.options.gui: if pf.options.mesa: os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1' from pyformex.gui import guimain res = guimain.createGUI() if res != 0: print(f"Could not start the pyFormex GUI: {res}") return res # EXIT if pf.GUI.console: # we can redirect output to the console if 'o' in pf.cfg['gui/redirect']: stdout = pf.GUI.console if 'e' in pf.cfg['gui/redirect']: stderr = pf.GUI.console.errorproxy sys.stdout.flush() sys.stderr.flush() with redirect_stdout(stdout), redirect_stderr(stderr): if not pf.options.gui: # Display the banner now pf.printwrap(pf.banner()) # Display the startup warnings if pf._startup.messages: pf.warning('\n\n'.join(pf._startup.messages)) # pf._startup.messages = [] # keep to show later? # # Qt may have changed the locale. # Since a LC_NUMERIC setting other than C may cause lots of troubles # with reading and writing files (formats become incompatible!) # we put it back to a sane setting # utils.setSaneLocale() # Startup done pf.started = True if pf.verbosity(2): print(f"pyFormex started from {pf._executable}") if pf.options.workdir: os.chdir(pf.options.workdir) # Prepend the inline script if pf.options.script: args[0:0] = ['-c', pf.options.script] # Prepend the autorun script ar = pf.cfg['autorun'] if ar and ar.exists(): args[0:0] = [ar] # remaining args are interpreted as scripts/apps and their parameters res = 0 if args: if pf.debugon(pf.DEBUG.INFO): print(f"Remaining args: {args}") from pyformex.script import processArgs res = processArgs(args) if res: pf.error(f"Exited with code {res} from arguments: {args}") # if we have a gui, go into gui interactive mode if pf.GUI: res = pf.GUI.run() if res: pf.error(f"GUI exited with code {res}") if pf.options.interactive: # Go into console interactive mode import readline import rlcompleter readline.set_completer(rlcompleter.Completer(context).complete) readline.parse_and_bind("tab: complete") shell = code.InteractiveConsole(context) shell.interact(banner="\nWelcome to the pyFormex interactive console") # Exit if pf.debugon(pf.DEBUG.INFO): print("Running (nongui) exit functions") for func in on_exit: func() if pf.debugon(pf.DEBUG.INFO): print(f"Exiting main with code {res}") return res
# End