Source code for pyformex

#
##
##  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 core module initialization.

This module initializes the pyFormex global variables and
defines a few essential functions.
It is the very first thing that is loaded when starting pyFormex.
"""
import os
import sys
import datetime
start_time = datetime.datetime.now()
__prog__ = "pyformex"
__version__ = "3.5"
__revision__ = __version__
__branch__ = "master"
Copyright = 'Copyright (C) 2004-2024 Benedict Verhegghe'
Url = 'http://pyformex.org'
Description = ("pyFormex is a tool for generating, manipulating and "
               "transforming large geometrical models of 3D structures "
               "by sequences of mathematical transformations.")


[docs]def Version(): """Return a string with the pyFormex name and version""" return f"pyFormex {__version__}"
[docs]def fullVersion(): """Return a string with the pyFormex name, version and revision""" if __revision__ == __version__: return Version() else: return f"{Version()} ({__revision__}) DEVELOPMENT VERSION"
startup_warnings = [] started = False # Fatal errors
[docs]class FatalError(Exception): """Raised on fatal or grave errors""" def __init__(self, msg): from inspect import cleandoc as dedent s = '\n' + '#' * 72 s = '\n## '.join([s] + dedent(msg).split('\n')) + s Exception.__init__(self, s)
[docs]class RequirementsError(FatalError): """Raised on missing or invalid requirements""" pass
[docs]class ImplementationError(FatalError): """Raised on clear implementation errors""" pass
######### Check Python version ############# # intended Python version # We support 3.8+ only, because we use self documenting f-strings _minimal_version = 0x03080000 _target_version = 0x030B0000 _numpy_version = '>=1.16'
[docs]def major(v): """Return the major component of version""" return v >> 24
[docs]def minor(v): """Return the minor component of version""" return (v & 0x00FF0000) >> 16
[docs]def human_version(v): """Return the human readable string for version""" return f"{major(v)}.{minor(v)}"
if sys.hexversion < _minimal_version: # Older than minimal y = human_version(sys.hexversion) m = human_version(_minimal_version) raise RequirementsError(f""" Your Python version is {y}, but pyFormex requires Python >= {m}. We advice you to upgrade your Python version. """) if sys.hexversion & 0xFFFF0000 > _target_version: # Major,minor newer than target: y = human_version(sys.hexversion) t = human_version(_target_version) startup_warnings.append(f""" Your Python version is {y}, but {Version()} has only been tested with Python <= {t}. We expect {Version()} to run correctly with your Python version, but if you encounter problems, please contact the developers at http://pyformex.org. """) ################################################################### ## Load Path class ################## from pyformex.path import Path # noqa: E402 # Placeholder for excutable, if pyformex: filled in by startup.py executable = Path(sys.executable) #### Detect install type ######## # Install type. # This can have the following values: # 'G' : unreleased version running from GIT sources: no installation # required. This is set if the directory containing the # pyformex start script is a git repository. # 'U' : normal (source) Release, i.e. tarball installed as user. # Installation is done with 'python setup.py install' # in the unpacked source. # The builtin pyFormex removal is activated. # 'R' : normal (source) Release, i.e. tarball (default). Installation # is done with 'python setup.py install' in the unpacked source. # The builtin pyFormex removal is activated. # 'D' : Distribution package of a released version, official or not # (e.g. Debian package). The distribution package tools should # be used to install/uninstall. The builtin pyFormex removal is # deactivated. # installtype = 'R' pyformexdir = Path(__file__).parent if (pyformexdir.parent / '.git').exists(): installtype = 'G' if installtype == 'R': if os.getuid() != 0 and (pyformexdir / 'doc').is_writable_dir(): installtype = 'U' ########## start the logger #################### # TODO: this is work in progress # import logging # noqa: E402 # LOGGING = { # 'version': 1, # 'disable_existing_loggers': False, # 'formatters': { # 'default': { # 'format': "%(asctime)s - %(name)s - %(levelname)s - %(message)s" # }, # 'short': { # 'format': "%(asctime)s %(message)s", # }, # 'detail': { # 'format': "%(asctime)s::%(levelname)s::%(name)s::" # "%(filename)s::%(lineno)d::%(message)s", # }, # }, # 'handlers': { # 'console': { # 'class': 'logging.StreamHandler', # 'formatter': 'detail', # }, # # 'memory': { # # 'class': 'logging.handlers.MemoryHandler', # # 'capacity': 100, # # 'formatter': 'default', # # }, # # TODO: activate logfile, but we cant use pf.cfg yet! # # maybe first use memory logger and later switch to file logger # # 'logfile': { # # 'level':'DEBUG', # # 'class':'logging.handlers.RotatingFileHandler', # # 'filename': pf.cfg['logfile'], # # 'maxBytes': 1024*1024*5, # 5 MB # # 'backupCount': 100, # # 'formatter':'standard', # # }, # }, # 'loggers': { # 'pyformex': { # 'level': 'INFO', # 'handlers': ['console'], # } # }, # } # import logging.config # noqa: E402 # logging.config.dictConfig(LOGGING) # logger = logging.getLogger('pyformex') # logger.info(f"{fullVersion()} start on Python{human_version(sys.hexversion)}") ########## special global flags ############## # Are we running tests? pytest = 'pytest' in sys.modules doctest = False # Are we importing from Sphinx doc generator? sphinx = 'sphinx' in sys.modules ########## acceleration libraries ############## libraries = ['misc_c', 'nurbs_c', 'clust_c'] ########## Special adjustements for source versions ############ if installtype == 'G': # We are running from a source tree (Git) import importlib.machinery
[docs] def run_cmd(cmd): """Run command""" import subprocess # print(f"{cmd=}") P = subprocess.Popen( str(cmd), stdout=subprocess.PIPE, shell=True, universal_newlines=True ) out, err = P.communicate() if P.returncode: print(out) print(err) return P.returncode, out, err
# Clean the source tree source_clean = pyformexdir.parent / 'source_clean' if source_clean.exists(): try: suffix = importlib.machinery.EXTENSION_SUFFIXES[0] cmd = f"{source_clean} --keep_modules '*{suffix}'" run_cmd(cmd) except Exception as e: print(str(e)) print(f"Error while executing {source_clean}\n" "Ignore error and continue") # Running from source tree: make sure the compiled libraries are up-to-date libdir = pyformexdir / 'lib' def checkLibraries(): # find extension used by compiled library (.so) import sysconfig ext = sysconfig.get_config_var('EXT_SUFFIX') if ext is None: ext = '.so' msg = '' for lib in libraries: ver = libdir / 'version.h' src = libdir / lib + '.c' obj = libdir / lib + ext if not obj.exists() or obj.mtime < src.mtime or obj.mtime < ver.mtime: msg += f"\nThe compiled library '{lib}' is not up to date!" return msg msg = checkLibraries() if msg and not sphinx: print(msg) print("Rebuilding pyFormex libraries, please wait") cmd = f"cd {pyformexdir.parent}; PYTHON={sys.executable} make lib" run_cmd(cmd) msg = checkLibraries() if msg: # No success msg += f""" I had a problem rebuilding the libraries in {libdir}. You should probably exit pyFormex, fix the problem and then restart pyFormex. """ if msg: startup_warnings.append(msg) # Set the proper revision number when running from git sources def set_revision(): global __revision__ cmd = f"cd {pyformexdir} && git describe --always" sta, out, err = run_cmd(cmd) if sta == 0: __revision__ = out.split('\n')[0].strip() else: print("Could not set revision") def set_branch(): global __branch__ cmd = f"cd {pyformexdir} && git rev-parse --abbrev-ref HEAD" sta, out, err = run_cmd(cmd) if sta == 0: __branch__ = out.split('\n')[0] else: print("Could not set branch name") set_revision() set_branch() # logger.info(f"This is {fullVersion()}") # logger.info(f"Installation type {installtype} at {pyformexdir}") ################ Special import mechanisms ############## def import_lazy(name): import importlib.util spec = importlib.util.find_spec(name) loader = importlib.util.LazyLoader(spec.loader) spec.loader = loader module = importlib.util.module_from_spec(spec) sys.modules[name] = module loader.exec_module(module) return module def import_direct(file): import importlib.util import tokenize file_path = tokenize.__file__ module_name = tokenize.__name__ spec = importlib.util.spec_from_file_location(module_name, file_path) module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) return module ################ pyFormex global variables and functions ############### # The GUI parts app_started = False interactive = False app = None # the QApplication GUI = None # the GUI QMainWindow canvas = None # the OpenGL Drawing widget controlled by the running script console = None # alternate Python console # initialize some global variables used for communication between modules # the options found on the command line # this is replaced with the options read from the command line # but we define it here with an attribute debuglevel, so that we can # use debug (unintentionally) before the debuglevel is set # Important pyFormex globals PF = {} # explicitely exported globals GS = None # only set in gui mode scriptName = None scriptlock = set() scriptMode = None # print function allowing a (nonfunctional) 'color' argument def _print_col(*args, color='black', **kargs): print(*args, **kargs) # Error printing in nongui mode def _print_err(*args, **kargs): file = kargs.pop('file', sys.stderr) print(*args, **kargs, file=file) printc = _print_col warning = _print_err error = _print_err 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 GUI: GUI.setBusy(state) else: _busy(state)
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")
###################################################################### ### DEBUG items ################################ from enum import Flag # noqa: E402 # TODO: test fails for Python 3.9: also shows NONE and ALL
[docs]class DEBUG(Flag): """A class with debug items. This class holds the defined debug items as attributes. Each debug topic is a binary value with a single bit set (a power of 2). Debug items can be combined with & (AND), | (OR) and ^ (XOR). Two extra values are defined: NONE switches off all debugging, ALL activates all debug items. Examples -------- List all existing debug items: >>> for v in DEBUG: ... if v not in (DEBUG.NONE, DEBUG.ALL): ... print(v) DEBUG.INFO DEBUG.WARNING DEBUG.CONFIG DEBUG.DETECT DEBUG.HELP DEBUG.MEM DEBUG.SCRIPT DEBUG.GUI DEBUG.MENU DEBUG.DRAW DEBUG.CANVAS DEBUG.OPENGL DEBUG.LIB DEBUG.MOUSE DEBUG.APPS DEBUG.IMAGE DEBUG.PICK DEBUG.MISC DEBUG.ABQ DEBUG.WIDGET DEBUG.PROJECT DEBUG.WEBGL DEBUG.MULTI DEBUG.LEGACY DEBUG.PLUGIN DEBUG.UNICODE DEBUG.FONT DEBUG.PGF DEBUG.VTK DEBUG.DOCTEST """ NONE = 0 (INFO, WARNING, CONFIG, DETECT, HELP, MEM, SCRIPT, GUI, MENU, DRAW, CANVAS, OPENGL, LIB, MOUSE, APPS, IMAGE, PICK, MISC, ABQ, WIDGET, PROJECT, WEBGL, MULTI, LEGACY, PLUGIN, UNICODE, FONT, PGF, VTK, DOCTEST, ) = [2**i for i in range(30)] ALL = -1
[docs] @classmethod def item(cls, name): """Convert a string to a DEBUG item The string is case insensitive. Raises ------ ValueError If the name is not a DEBUG item. """ try: return cls[name.upper()] except KeyError: raise ValueError(f"No such {cls.__name__} item: {name}")
[docs] @classmethod def level(cls, names): """Return the combined debug level for a list of debug items Raises ------ ValueError If any of the names is not a DEBUG item. """ from operator import or_ from functools import reduce return reduce(or_, [cls.item(name) for name in names])
class options: debuglevel = DEBUG.NONE # Make sure we can use options.debuglevel verbose = 1 # Make sure we can use uselib = False # initial default to keep sphinx happy
[docs]def debugon(topics): """Return True if any of specified debug topics is in options.debuglevel. Parameters ---------- topics: :class:`DEBUG` A DEBUG topic or any combination of them. Returns ------- bool: True if any of the flags in topics is in options.debuglevel. This is effectively the bool value of the binary AND of topics and options.debuglevel. Note ---- If you need to test that multiple flags are in options.debuglevel, use ``debugon(flag1) & debugon(flag2)`` Examples -------- >>> options.debuglevel = DEBUG.INFO | DEBUG.OPENGL | DEBUG.GUI | DEBUG.MENU >>> debugon(DEBUG.WARNING | DEBUG.OPENGL) True >>> debugon(DEBUG.WARNING & DEBUG.OPENGL) False >>> debugon(DEBUG.WARNING & DEBUG.OPENGL | DEBUG.GUI) True >>> debugon(DEBUG.OPENGL) & debugon(DEBUG.GUI) True >>> debugon(DEBUG.ALL) True >>> options.debuglevel = DEBUG.NONE >>> debugon(DEBUG.ALL) False """ return bool(options.debuglevel & topics)
# Deprecated 2023-12-04 def debug(s, level=DEBUG.ALL): from pyformex import utils utils.warn('depr_debug', stacklevel=3) if debugon(level): print(f"{level}: {s}")
[docs]def verbosity(verbose): """Check that verbosity is equal or higher than given value""" return options.verbose >= verbose
# Deprecated 2023-12-04
[docs]def verbose(verbose, *args, **kargs): """Print a message if the verbosity is high enough""" from pyformex import utils utils.warn('depr_verbose', stacklevel=3) if verbosity(verbose): print(*args, **kargs)
[docs]def run(args): """Run pyFormex from a Python shell Parameters ---------- args: str The arguments for pyformex like thy would be entered on the pyformex command line in a shell. """ import shlex from pyformex import main # We do not return a value here: it is only used when pyformex is # imported in a Python shell main.run(shlex.split(args))
#################################################################### ### Load factory configuration ############################### from pyformex.config import Config # noqa: E402 # Create a config instance cfg = Config() cfg['pyformexdir'] = pyformexdir # load the factory defaults defaults = cfg['pyformexdir'] / 'pyformexrc' cfg.load(defaults) # The default user config path (do not change!) cfg['userprefs'] = cfg['userconfdir'] / 'pyformex.conf' # Set the current session configurations in pyformex module prefcfg = None # the preferenced configuration refcfg = None # the reference configuration preffile = None # the file where the preferenced configuration will be saved # End