#
##
## SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.4 (Thu Nov 16 18:07:39 CET 2023)
## 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.dev4"
__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
[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.
"""
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