Source code for script

#
##
##  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/.
##
"""Basic pyFormex script functions

The :mod:`script` module provides the basic functions available
in all pyFormex scripts. These functions are available in GUI and NONGUI
applications, without the need to explicitely importing the :mod:`script`
module.
"""
import os
import sys
import time

import numpy as np

import pyformex as pf
from pyformex import utils
from pyformex.path import Path
from pyformex.timing import Timing
from pyformex.geometry import Geometry

# __all__ = ('chdir', 'mkdir', 'runAny', 'breakpt', 'exit',
#            'scriptLock', 'scriptRelease')
sleep = time.sleep

######################### Exceptions #########################################


[docs]class ScriptExit(Exception): """Exception raised to exit from a running script.""" pass
scripttimer = Timing('_script_') # timing the script execution scriptname = None # name of running script/app scriptmode = None # running script mode: script/app scriptlock = set() # script locks scriptglobals = {} # users can set here globals #################### Interacting with the user ###############################
[docs]def ask(question, choices=None, default=''): """Ask a question and present possible answers. If no choices are presented, anything will be accepted. Else, the question is repeated until one of the choices is selected. If a default is given and the value entered is empty, the default is substituted. Case is not significant, but choices are presented unchanged. If no choices are presented, the string typed by the user is returned. Else the return value is the lowest matching index of the users answer in the choices list. Thus, ask('Do you agree',['Y','n']) will return 0 on either 'y' or 'Y' and 1 on either 'n' or 'N'. """ if choices: question += f" ({', '.join(choices)}) " choices = [c.lower() for c in choices] while True: res = input(question) if res == '' and default: res = default if not choices: return res try: return choices.index(res.lower()) except ValueError: pass
[docs]def ack(question): """Show a Yes/No question and return True/False depending on answer.""" return ask(question, ['Y', 'N']) == 0
[docs]def error(message): """Show an error message and wait for user acknowlegement.""" print("pyFormex Error: "+message) if not ack("Do you want to continue?"): exit()
def warning(message): print("pyFormex Warning: "+message) if not ack("Do you want to continue?"): exit() # TODO: replace this with a real warn function # Error printing in nongui mode def _print_err(*args, **kargs): kargs.pop('uplevel', None) kargs.setdefault('file', sys.stderr) pf.printc(*args, color='red', **kargs) pf.printc = pf.color.printc pf.warning = _print_err pf.error = _print_err def showInfo(message): print("pyFormex Info: "+message) def _busy(state=True): """Flag a busy state to the user""" if state: print("This may take some time...")
[docs]def busy(state=True): """Flag a busy state to the user""" if pf.GUI: pf.GUI.setBusy(state) else: _busy(state)
def Globals(): g = { 'np': np, 'pf': pf, } return g ########################### PLAYING SCRIPTS ############################## exitrequested = False scriptInit = None # can be set to execute something before each script
[docs]def playScript(scr, name=None, filename=None, argv=None, encoding=None): """Run a pyFormex script specified as text. Parameters ---------- scr: str A multiline string holding a valid Python program, ususally a pyFormex script. The program will be executed in an internal interpreter, with the :func:`Globals` as local definitions. There is a lock to prevent multiple scripts from being executed at the same time. This implies that pyFormex scripts can not start another script. name: str, optional If specified, this name is set in the global variable scriptname to identify the currently running script. filename: :term:`path_like`, optional If specified, this filename is set into the global variable __file__ in the script executing environment. argv: list of str, optional If provided, the global variable pf._argv_ is set to this list. It is mostly used to pass an argument list to a script in --nogui mode. encoding: str, optional If specified, this is an encoding scheme for the script text provided in ``scr``. The text will be decoded by the specified scheme prior to execution. This is mostly intended to obfuscate the code for occasional viewer. """ global exitrequested, scriptname # Scripts are not reentrant, so we only allow one running at a time! if len(scriptlock) > 0: print("!!Not executing because a script lock has been set: " f"{scriptlock}") return 1 scriptLock('__auto/script__') exitrequested = False # Get the globals g = Globals() if argv is not None: pf._argv_ = argv g.update({ '__name__': '__draw__' if pf.GUI else '__script__', '__file__': filename, }) if pf.GUI: pf.GUI.startRun() if encoding == 'egg': import base64 scr = base64.b64decode(scr) if isinstance(scr, bytes): scr = scr.decode('utf-8') # Now we can execute the script using these collected globals scriptname = name exitall = False if pf.debugon(pf.DEBUG.MEM): memu0 = utils.memUsed() print(f"MemUsed = {memu0}") if filename is None: filename = '<string>' # Execute the code try: if not pf.options.future: pf.interpreter.locals.update(g) pf.interpreter.runsource(scr, str(filename), 'exec') except ScriptExit: pf.printc("EXIT FROM SCRIPT", color='red') except SystemExit: pf.printc("EXIT FROM SCRIPT", color='red') finally: # honour the exit function if 'atExit' in g: atExit = g['atExit'] try: atExit() except Exception: if pf.debugon(pf.DEBUG.SCRIPT): print('Error while calling script exit function') if pf.cfg['autoglobals']: if pf.GUI and pf.GUI.console: g = pf.GUI.console.interpreter.locals autoExport(g) scriptRelease('__auto/script__') # release the lock if pf.GUI: pf.GUI.stopRun() if pf.debugon(pf.DEBUG.MEM): memu1 = utils.memUsed() print(f"MemUsed = {memu1} (diff= {memu1-memu0})") if exitall: if pf.debugon(pf.DEBUG.SCRIPT): print("Calling quit() from playScript") quit() return 0
[docs]def run_script(fn, argv=[]): """Run a pyFormex script stored on a file. Parameters ---------- fn: :term:`path_like` The name of a file holding a pyFormex script. argv: list, optional A list of arguments to be passed to the script. This argument list becomes available in the script as the global variable _argv_. Notes ----- This calls :func:`playScript` to execute the code read from the script file. See Also -------- run_app: run a pyFormex application runAny: run a pyFormex script or app """ msgcolor = pf.Color('blue') fn = Path(fn) if pf.GUI: pf.GUI.scripthistory.add(str(fn)) pf.printc(f"Running script ({fn})", color=msgcolor) if pf.debugon(pf.DEBUG.SCRIPT): print(f" Executing with arguments: {argv}") encoding = None with scripttimer: res = playScript(fn.read_text(), fn, fn, argv, encoding) pf.printc(f"Finished script {fn} in {scripttimer.mem:.6f} seconds", color=msgcolor) if pf.debugon(pf.DEBUG.SCRIPT): print(f" Arguments left after execution: {argv}") return res
[docs]def run_app(appname, argv=[], refresh=False, lock=True, check=True, wait=False): """Run a pyFormex application. A pyFormex application is a Python module that can be loaded in pyFormex and at least contains a function 'run()'. Running the application means execute this function. Parameters ---------- appname: str The name of the module in Python dot notation. The module should live in a path included in the 'appsdirs' configuration variable. argv: list, optional A list of arguments to be passed to the app. This argument list becomes available in the app as the global variable _argv_. refresh: bool If True, the app module will be reloaded before running it. lock: bool If True (default), the running of the app will be recorded so that other apps can be locked out while this one has not finished. check: bool If True (default), check that no other app is running at this time. If another app is running, either wait or return without executing. wait: bool If True, and check is True and another app is busy, wait until that one has finished and then start execution. Returns ------- int The return value of the run function. A zero value is supposed to mean a normal exit. See Also -------- run_script: run a pyFormex script runAny: run a pyFormex script or app """ from pyformex import apps def _is_locked(): """Check for scriptlock. If wait is True, wait till it is free""" while len(scriptlock) > 0: if wait: print(f"!!Waiting for lock {scriptlock} to be released") time.sleep(5) else: print("!!Not executing because a script lock has been set: " f"{scriptlock}") return True return False def _recover_failing_app_load(): """Popup recovery dialog when failing to load app. Returns one of R(un as script), L(oad in editor), D(on't bother) """ errmsg = f"An error occurred while loading application {appname}" if apps._traceback and pf.cfg['showapploaderrors']: print(apps._traceback) if pf.GUI: pg = pf.gui fn = apps.findAppSource(appname) if fn.exists(): errmsg += ("\n\nYou may try executing the application " "as a script,\n or you can load the source " "file in the editor.") ans = pg.ask(errmsg, choices=[ 'Run as script', 'Load in editor', "Don't bother"])[0] if ans == 'L': pg.editFile(fn) elif ans == 'R': pf.GUI.setcurfile(fn) run_script(fn) else: errmsg += "and I can not find the application source file." pf.error(errmsg) else: pf.error(errmsg) global scriptname if pf.debugon(pf.DEBUG.APPS): print(f"run_app '{appname}'") if check and _is_locked(): # scriptlock is set return print(f"Loading application {appname} with refresh={refresh}") app = apps.load(appname, refresh=refresh) if app is None: _recover_failing_app_load() return # app loaded: now execute if lock: scriptLock('__auto/app__') scriptname = appname if pf.GUI: pf.GUI.startRun() pf.GUI.apphistory.add(appname) pf.printc(f"Running application '{appname}' from {app.__file__}", color='darkgreen') g = Globals() app.__dict__.update(g) if pf.debugon(pf.DEBUG.SCRIPT): print(f" Passing arguments: {argv}") app._argv_ = argv try: with scripttimer: res = app.run() except ScriptExit: print("EXIT FROM APP") except SystemExit: print("EXIT FROM APP") finally: if hasattr(app, 'atExit'): app.atExit() if pf.cfg['autoglobals']: g = app.__dict__ autoExport(g) if lock: scriptRelease('__auto/app__') # release the lock if pf.GUI: pf.GUI.stopRun() if pf.debugon(pf.DEBUG.SCRIPT): print(f" Arguments left after execution: {argv}") pf.printc(f"Finished {appname} in {scripttimer.mem:.6f} seconds", color='darkgreen') return res
[docs]def runAny(appname=None, argv=[], remember=True, refresh=False, wait=False): """Run a pyFormex application or script file. Parameters ---------- appname: str Either the name of a pyFormex application (app) or the name of a file containing a pyFormex script. An app name is specified in Python fotted module format (pkg.module) and the path to the package should be in the configuration variable 'appsdirs'. If no appname is provided, the current app/script set in the GUI will be run, if it is set. argv: list, optional A list of arguments to be passed to the app. This argument list becomes available in the script or app as the global variable _argv_. remember: bool If True (default), the app is set as the current app in the GUI, so that the play button can be used to run it again. refresh, wait: bool Parameters passed to :func:`run_app`. See Also -------- run_script: run a pyFormex script run_app: run a pyFormex application """ if appname is None: appname = pf.cfg['curfile'] if not appname: return if callable(scriptInit): scriptInit() if pf.GUI and remember: pf.GUI.setcurfile(appname) if utils.is_script(appname): return run_script(appname, argv) else: return run_app(appname, argv, refresh=refresh, wait=wait)
[docs]def autoExport(g): """Autoexport globals from script/app globals. Parameters ---------- g: dict A dictionary holding definitions topossibly autoexport. Normally this is the globals dict from a script/app run enviroment. Notes ----- This exports some objects from the script/app runtime globals to the pf.PF session globals directory. The default is to export all instances of :class:`Geometry`. This can be customized in the script/app by setting the global variables ``autoglobals`` and ``autoclasses``. If ``autoglobals`` evaluates to False, no autoexport will be done. If set to True, the default autoexport will be done: all instances of :class:`Geometry`. If set to a list of names, only the specified names will be exported. The global variable ``autoclasses`` may be set to a list of class names and all global instances of the specified classes will be exported. Remember that the variables need to be globals in your script/app in order to be autoexported, and that the autoglobals feature should not be disabled in your configuration (it is enabled by default). """ ag = g.get('autoglobals', True) if ag: if ag is True: # default autoglobals: all Geometry instances ag = [Geometry] an = [] for a in ag: if isinstance(a, str) and a in g: an.append(a) elif isinstance(a, type): an.extend([k for k in g if isinstance(g[k], a)]) if an: an = sorted(list(set(an))) print(f"Autoglobals: {', '.join(an)}") pf.PF.update([(k, g[k]) for k in an])
def scriptLock(id): global scriptlock, scriptmode if id == '__auto/script__': scriptmode = 'script' elif id == '__auto/app__': scriptmode = 'app' if pf.debugon(pf.DEBUG.SCRIPT): print(f"Setting script lock {id}") scriptlock |= {id} def scriptRelease(id): global scriptlock, scriptmode if pf.debugon(pf.DEBUG.SCRIPT): print(f"Releasing script lock {id}") scriptlock -= {id} scriptmode = None def force_finish(): global scriptlock scriptlock = set() # release all script locks (in case of an error)
[docs]def breakpt(msg=None): """Set a breakpoint where the script can be halted on a signal. If an argument is specified, it will be written to the message board. The exitrequested signal is usually emitted by pressing a button in the GUI. """ global exitrequested if exitrequested: if msg: pf.printc(msg, color='red') exitrequested = False # reset for next time raise SystemExit
def raiseExit(): pf.printc("Exit requested, will stop at next breakpoint", color='red') stopatbreakpt() if pf.GUI: pf.GUI.drawlock.release() # raise SystemExit("Exit requested from script") # raise ScriptExit("Exit requested from script") def enableBreak(mode=True): if pf.GUI: pf.GUI.enableButtons(pf.GUI.actions, ['Stop'], mode)
[docs]def stopatbreakpt(): """Set the exitrequested flag.""" global exitrequested exitrequested = True
[docs]def exit(all=False): """Exit from the current script or from pyformex if no script running.""" if len(scriptlock) > 0: if all: utils.warn("warn_exit_all") pass else: # This is the only exception we can use in script mode # to stop the execution raise SystemExit
[docs]def quit(): """Quit the pyFormex program This is a hard exit from pyFormex. It is normally not called directly, but results from an exit(True) call. """ if pf.app and pf._app_started: # quit the QT app if pf.debugon(pf.DEBUG.SCRIPT): print("exit called while no script running") pf.app.quit() # closes the GUI and exits pyformex else: # the QT app didn't even start sys.exit(0) # use Python to exit pyformex
[docs]def processArgs(args): """Run pyFormex scripts/apps in batch mode. Arguments are interpreted as names of script files, possibly interspersed with arguments for the scripts. Each running script should pop the required arguments from the list. """ res = 0 while len(args) > 0: if pf.debugon(pf.DEBUG.SCRIPT): print(f"Remaining args: {args}") fn = args.pop(0) if fn == '-c': # next arg is a script txt = args.pop(0) if '++' in args: # this is experimental and not documented i = args.index('++') argv = args[:i] args = args[i+1:] else: argv = args if fn == '-c': res = playScript(txt, name='__inline__', argv=argv) else: res = runAny(fn, argv=argv, remember=False) if res: print(f"Error during execution of script/app {fn}") return res
[docs]def setPrefs(res, save=False): """Update the current settings (store) with the values in res. res is a dictionary with configuration values. The current settings will be update with the values in res. If save is True, the changes will be stored to the user's configuration file. """ if pf.debugon(pf.DEBUG.CONFIG): print(f"Accepted settings:\n{res}") for k in res: pf.cfg[k] = res[k] if save and pf.prefcfg[k] != pf.cfg[k]: pf.prefcfg[k] = pf.cfg[k] if pf.debugon(pf.DEBUG.CONFIG): print(f"New settings:\n{pf.cfg}") if save: print(f"New preferences:\n{pf.prefcfg}")
########################## print information ################################ def printConfig(): print("Reference Configuration: " + str(pf.refcfg)) print("Preference Configuration: " + str(pf.prefcfg)) print("User Configuration: " + str(pf.cfg)) def printLoadedApps(): from pyformex import apps, sys loaded = apps.listLoaded() refcnt = [sys.getrefcount(sys.modules[k]) for k in loaded] print(', '.join([f"{k} ({r})" for k, r in zip(loaded, refcnt)])) ### Utilities
[docs]def chdir(path, create=False): """Change the current working directory. If path exists and it is a directory name, make it the current directory. If path exists and it is a file name, make the containing directory the current directory. If path does not exist and create is True, create the path and make it the current directory. If create is False, raise an Error. Parameters ---------- path: :term:`path_like` The name of a directory or file. If it is a file, the name of the directory containing the file is used. If the path exists, it is made the current directory. The path can be an absolute or a relative pathname. A '~' character at the start of the pathname will be expanded to the user's home directory. create: bool. If True and the specified path does not exist, it will be created and made the current directory. The default (False) will do nothing if the specified path does not exist. Notes ----- The new current directory is stored in the user's preferences file for persistence between pyFormex invocations. """ path = Path(path).expanduser() if path.exists(): if not path.is_dir(): path = path.resolve().parent else: if create: mkdir(path) else: raise ValueError(f"The path {path} does not exist") try: os.chdir(str(path)) setPrefs({'workdir': path}, save=True) except Exception: pass print("Current directory is", Path.cwd()) if pf.GUI: pf.GUI.setcurdir()
[docs]def mkdir(path, clear=False, new=False): """Create a directory. Create a directory, including any needed parent directories. Any part of the path may already exist. Parameters ---------- path: :term:`path_like` The pathname of the directory to create, either an absolute or relative path. A '~' character at the start of the pathname will be expanded to the user's home directory. clear: bool. If True, and the directory already exists, its contents will be deleted. new: bool. If True, requires the directory to be a new one. An error will be raised if the path already exists. The following table gives an overview of the actions for different combinations of the parameters: ====== ==== =================== ============= clear new path does not exist path exists ====== ==== =================== ============= F F newly created kept as is T F newly created emptied T/F T newly created raise OSError ====== ==== =================== ============= Returns ------- Path The tilde-expanded path of the directory, if the operation was successful. Raises ------ OSError: in the following cases: - the directory could not be created, - ``clear=True``, and the existing directory could not be cleared, - ``new=False, clear=False``, and the existing path is not a directory, - ``new=True``, and the path exists. """ path = Path(path).expanduser() if new and path.exists(): raise OSError(f"Path already exists: {path}") if clear and path.exists(): path.removeTree(top=not new) os.makedirs(path, exist_ok=not new) return path
[docs]def runtime(): """Return the time elapsed since start of execution of the script.""" return scripttimer.value()
[docs]def startGui(): """Start the gui This can be used to start the gui when pyFormex was loaded from a Python shell. """ if pf.GUI is None: if pf.debugon(pf.DEBUG.GUI): print("Starting the pyFormex GUI") from pyformex.gui import guimain guimain.createGUI() if pf.GUI: pf.GUI.run() del pf.GUI del pf.app pf.GUI = pf.app = None
##################################################### ## deprecated 2024-02-22 @utils.deprecated_by('pf.PF.forget') def forget(names): pf.PF.forget(*names)
[docs]@utils.deprecated_by('pf.PF.clear') def forgetAll(): """Delete all the global variables.""" pf.PF.clear()
@utils.deprecated_by('pf.PF.rename') def rename(oldnames, newnames): for oldname, newname in zip(oldnames, newnames): pf.PF.rename(oldname, newname) @utils.deprecated_by('pf.PF.contents or pf.Project(data=dic).contents') def listAll(clas=None, like=None, filtr=None, dic=None, sort=False): if dic is None: dic = pf.PF else: dic = pf.Project(data=dic) return dic.contents(clas=clas, like=like, filtr=filtr, sort=sort) @utils.deprecated_by('pf.PF[] or pf.PF.get()') def named(name): return pf.PF[name]
[docs]@utils.deprecated_by('pf.PF.update') def export(dic): """Export the variables in the given dictionary.""" pf.PF.update(dic)
[docs]@utils.deprecated_by('pf.PF.update2') def export2(names, values): """Update the project with a list of names and values.""" pf.PF.update(zip(names, values))
@utils.deprecated_by('print(Path.cwd())') def pwdir(): print(f"Current workdir is {Path.cwd()}")
[docs]@utils.deprecated_by('pf.cfg[] or pf.cfg.get') def getcfg(name, default=None): """Return a value from the configuration or None if nonexistent.""" try: return pf.cfg[name] except KeyError: return default
#### End