Source code for software

#
##
##  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/.
##
"""Detecting and checking installed software

A module to help with detecting required software and helper software,
and to check the versions of it.
"""
import os
import sys
import importlib
import re
import operator

import pyformex as pf
from pyformex.path import hsortkey
from pyformex.metaclass import RegistryMeta

class RequirementError(Exception):
    pass


[docs]class Version(list): """Version numbering with sane comparison. Like so many other projects pyFormex has long time been using distutils.SaneVersion to compare version strings with possibly unknown and complex structure. After distutils got removed from Python we have had the source copied into pyFormex for some time. Then I had another look at it and realized that the :func:`pyformex.path.hsortkey` function was an ideal candidate to compare version numbers: it was developed to sort file names in a human way, sorting the numerical parts in the the strings in numerical order and the non-numeric parts in lexical order. It even treats dots as a special case. So here's a class that treats any string as a version number. It even handles cases where SaneVersion fails, because we guarantee the type of the subsequent components of the splitted string. And we can rebuild the original version string from the components. A Version instance is a list of alternating str and int components, starting and ending wit a str. Empty strings are inserted where needed. Dots are converted into a -1 int value, so they are sorted before another value. Examples -------- >>> v = Version('1.29a3') >>> v ['', 1, '', -1, '', 29, 'a', 3, ''] >>> print(v) 1.29a3 >>> print(v.pure()) 1.29.3 >>> print(v.pure(2)) 1.29 >>> v > Version('1.29') True >>> v < Version('1.30') True >>> v < Version('1.29b') True """ def __init__(self, version): super().__init__(hsortkey(version))
[docs] def pure(self, max=3): """Return purely numeric parts""" return '.'.join(list(str(c) for c in self if isinstance(c, int) and c >= 0)[:max])
def __str__(self): """Return the full version string""" return ''.join('.' if isinstance(c, int) and c < 0 else str(c) for c in self)
[docs]def is_namespace(module): """Check if a module is a namespace package""" return (hasattr(module, "__path__") and getattr(module, "__file__", None) is None)
[docs]class Software(metaclass=RegistryMeta): """Register for software versions. This class holds a register of the version of installed software. The class is not intended to be used directly, but rather through the derived classes Module and External. Parameters ---------- name: str The software name as known in pyFormex: this is often the same as the real software name, but can be different if the real software name is complex. We try to use simple lower case names in pyFormex. Examples -------- >>> np = Software('numpy') >>> Software.print_all() numpy (** Not Found **) >>> Software.has('numpy') '' >>> np.detect('detected') 'detected' >>> Software.has('numpy') 'detected' >>> Software.require('numpy') >>> Software.has('foo') Traceback (most recent call last): pyformex.software.RequirementError: foo is not a registered Software >>> foo = Software('foo') >>> Software.require('foo') Traceback (most recent call last): pyformex.software.RequirementError: Required Software 'foo' (foo) not found >>> Software.print_all() numpy (detected) foo (** Not Found **) >>> Software('foo') Traceback (most recent call last): ... ValueError: Software('foo') already exists """ def __init__(self, name): """Create a registered software name""" self.name = name self._version = None def __str__(self): return f"{self.__class__.__name__} {self.name} {self._version}" @classmethod def registered(clas): return list(clas.dict.keys()) @property def version(self): """Return the version of the software, if installed. If the software has already been detected, just returns the stored version string. Else, runs the software :meth:`detect` method and stores and returns the version string. Returns ------- str The version of the software, or an empty string if the software can not be detected. """ if self._version is None: self.detect() return self._version @version.setter def version(self, value): self._version = value
[docs] def detect(self, version='', fatal=False, quiet=False): """Detect the version of the software. Parameters ---------- version: str The version string to be stored for a detected software. The default empty string means the software was not detected. Derived classes should call this method fromt their detect method and pass a non-empty string for detected softwares. fatal: bool, optional If True and the software can not be loaded, a fatal exception is raised. Default is to silently ignore the problem and return an empty version string. quiet: bool, optional If True, the whole operation is done silently. The only information about failure will be the returned empty version string. Returns ------- str The version string of the software, empty if the software can not be loaded. Notes ----- As a side effect, the detected version string is stored for later reuse. Thus subsequent tests will not try to re-detect. """ if version: if pf.debugon(pf.DEBUG.DETECT): print(f"Congratulations! You have {self.name} ({version})") else: if not fatal: if pf.debugon(pf.DEBUG.DETECT): print(f"ALAS! I could not find {self.__class__.__name__} " f"'{self.name}' on your system") if fatal: raise pf.RequirementsError( f"""\ A required {self.__class__.__name__} was not found: {self.name} You should install this software and restart pyFormex.\ """) self.version = version return self.version
[docs] @classmethod def detect_all(clas, names=None): """Detect all registered or required softwares. Parameters ---------- names: list of str, optional The list of names of registered software components of type **clas** that need to be detected. If not provided, all the registered components of that class are probed. Notes ----- Usually, detection is only performed when needed. This method can however be used to detect a list of requirements or to report a full detection report on all registered softwares. """ if names is None: names = clas.dict for name in names: clas.dict[name].detect()
[docs] @classmethod def detected(clas, names=None, probe=True): """Return the current detection state of softwares and their version. Parameters ---------- names: list of str, optional The list of names of registered software components of this **clas** that need to be returned. If not provided, all the registered components of that class are returned. Returns ------- dict A dict with the software name as key and the detected version as value. """ if probe: clas.detect_all(names) items = clas.dict.items() if names is not None: items = ((k, v) for (k, v) in items if k in names) return dict((k, v._version) for (k, v) in items)
[docs] @classmethod def print_all(clas): """Print the list of registered softwares""" for name in clas.dict: version = clas[name].version if not version: version = '** Not Found **' print(f" {name} ({version})")
[docs] @classmethod def has(clas, name, check=False, fatal=False, quiet=False): """Test if we have the named software available. Returns a nonzero (version) string if the software is available, or an empty string if it is not. By default, the software is only checked on the first call. The optional argument check==True forces a new detection. """ if name not in clas.dict: raise RequirementError(f"{name} is not a registered {clas.__name__}") if check or clas[name].version is None: clas[name].detect(fatal=fatal, quiet=quiet) return clas[name].version
[docs] @classmethod def check(clas, name, version): """Check that we have a required version of a software. """ ver = clas.has(name) return compareVersion(ver, version)
[docs] @classmethod def require(clas, name, version=None): """Ensure that the named Python software/version is available. Checks that the specified software is available, and that its version number is not lower than the specified version. If no version is specified, any version is ok. Returns if the required software/version could be loaded, else an error is raised. """ if pf._sphinx: # Do not check when building docs return ver = clas.has(name) if not ver: realname = clas[name].name errmsg = f""".. **{clas.__name__} {name} not found!** You activated some functionality requiring the {clas.__name__} '{realname}'. However, the {clas.__name__} '{realname}' could not be found. Probably it is not installed on your system. """ pf.error(errmsg, uplevel=1) raise RequirementError( f"Required {clas.__name__} '{name}' ({realname}) not found") else: if version is not None: if not compareVersion(ver, version): realname = clas[name].name errmsg = f""".. **{clas.__name__} version {name} ({ver}) does not meet requirement {version}!** You activated some functionality requiring {clas.__name__} '{realname}'. However, the required version for that software could not be found. """ pf.error(errmsg, uplevel=1) raise RequirementError( f"Required version {version} of {clas.__name__} " f"'{name}' ({realname}) not found")
[docs]def del_mod_parents(modname): """Delete a module and its parents""" while True: if modname not in sys.modules: break del sys.modules[modname] modname, *tail = modname.rsplit('.', 1) if not tail: break
[docs]class Module(Software): """Register for Python module version detection rules. This class holds a register of version detection rules for installed Python modules. Each instance holds the rule for one module, and it is automatically registered at instantiation. The modules used by pyFormex are declared in this module, but users can add their own just by creating a Module instance. Parameters ---------- name: str The module name as known in pyFormex: this is often the same as the Python module name, but can be different if the Python module name is complex. We try to use simple lower case names in pyFormex. modname: str, optional The correct Python package.module name. If not provided, it is equal to the pyFormex name. attr: str or tuple of str, optional If a str, it is the name of the attribute holding the module version. This should be an attribute of the module `modname`. The default is '__version__', as it is used by many projects. If the version is not stored in a direct attribute of the same module as used for the detection, then a tuple of strings can be specified, starting with the Python module name in which the version attribute is stored, and a list of subsequent attribute names leading to the version. In this case the first element of the tuple is always a module name. If it is the same as `modname`, an empty string may be specified. If the final attribute is a callable, it will be called to get the version. The result is always converted to str before being stored as the version. incompatible: tuple of str, optional A list of incompatible modules. If any of these modules is loaded, the Module will not be tried. Beware: this should be the actual module names, not the pyFormex component name, which is all lower case. Examples -------- >>> Module.dict.clear() >>> Module.detect_all() >>> Module.print_all() >>> np = Module('numpy') >>> pil = Module('pil', modname='PIL', attr='VERSION') >>> Module.print_all() numpy (...) pil (** Not Found **) >>> Version(Module.has('numpy')) >= Version('1.16') True >>> Module.print_all() numpy (...) pil (** Not Found **) >>> Module.has('foo') Traceback (most recent call last): pyformex.software.RequirementError: foo is not a registered Module >>> Module.require('foo') Traceback (most recent call last): pyformex.software.RequirementError: foo is not a registered Module >>> foo = Module('foo','FooBar') >>> Module.has('foo') '' >>> Module.require('foo') Traceback (most recent call last): pyformex.software.RequirementError: Required Module 'foo' (FooBar) not found Now fake a detection of Module 'foo' >>> Module['foo'].version = '1.2.3' >>> Module.has('foo') '1.2.3' >>> Module.require('foo') >>> Module.require('foo','>= 1.1.7') >>> Module.require('foo','>= 1.3.0') Traceback (most recent call last): pyformex.software.RequirementError: Required version >= 1.3.0 \ of Module 'foo' (FooBar) not found """ unload = False def __init__(self, name, modname=None, attr=None, incompatible=None): """Create a registered Python module detection rule""" super().__init__(name) if modname is None: modname = name # default: name == modname if attr is None: attr = '__version__' # many packages use this self.name = modname self.attr = attr self.incompatible = incompatible
[docs] def detect(self, fatal=False, quiet=False): """Detect the version of the module. Parameters ---------- fatal: bool, optional If True and the module can not be loaded, a fatal exception is raised. Default is to silently ignore the problem and return an empty version string. quiet: bool, optional If True, the whole operation is done silently. The only information about failure will be the returned empty version string. Returns ------- str The version string of the module, empty if the module can not be loaded. Notes ----- As a side effect, the detected version string is stored for later reuse. Thus subsequent tests will not try to re-detect. """ try: if pf.debugon(pf.DEBUG.DETECT): print(f"Detect Module {self.name}") if self.incompatible: if any([m in sys.modules for m in self.incompatible]): raise ImportError( f"Module {self.name} is incompatible with" f" {self.incompatible}") m = importlib.import_module(self.name) if pf.debugon(pf.DEBUG.DETECT): print(m) if is_namespace(m): msg = f"Module {self.name} is a namespace package at {m.__path__}" del_mod_parents(self.name) raise ImportError(msg) if self.unload and self.name[:4] in ('PySi', 'PyQt'): # print(f"UNLOADING {self=}, {self.name=}") del_mod_parents(self.name) if isinstance(self.attr, str): # simple attribute in loaded module ver = (self.attr,) else: # tuple of subsequent attributes, first is module name if self.attr[0]: m = importlib.import_module(self.attr[0]) if pf.debugon(pf.DEBUG.DETECT): print(m) ver = self.attr[1:] for a in ver: m = getattr(m, a) if pf.debugon(pf.DEBUG.DETECT): print(m) except (ModuleNotFoundError, ImportError, AttributeError) as e: # failure: unregistered, unexisting, missing attribute if fatal: raise e else: if pf.debugon(pf.DEBUG.DETECT): print(e) m = '' # If the attribute is a callable, call it if callable(m): m = m() # if a tuple is returned, turned it into a string if isinstance(m, tuple): m = '.'.join(map(str, m)) # make sure version is a string (e.g. gl2ps uses a float!) version = str(m) super().detect(version, fatal) return self.version
Module('pyformex').version = pf.__version__ # avoid detection Module('calpy', modname='calpy') Module('docutils') Module('fontforge') Module('gl2ps', attr='GL2PS_VERSION') Module('gnuplot', modname='Gnuplot') Module('h5py') # Module('ipython', modname='IPython',) # Module('ipython-qt', modname='IPython.frontend.qt',) Module('matplotlib') Module('meshio') Module('moderngl') Module('numpy') Module('pil', modname='PIL') Module('pydicom') Module('pyopengl', modname='OpenGL') Module('pyqt5', modname='PyQt5.QtCore', attr='PYQT_VERSION_STR', incompatible=('PyQt6', 'PySide2', 'PySide6')) Module('pyqt5gl', modname='PyQt5.QtOpenGL', attr=('PyQt5.QtCore', 'PYQT_VERSION_STR'), incompatible=('PyQt6', 'PySide2', 'PySide6')) if sys.version_info.minor < 12: Module('pyside2', modname='PySide2', incompatible=('PyQt5', 'PyQt6', 'PySide6')) Module('pyqt6', modname='PyQt6.QtCore', attr='PYQT_VERSION_STR', incompatible=('PyQt5', 'PySide2', 'PySide6')) Module('pyside6', modname='PySide6', incompatible=('PyQt5', 'PyQt6', 'PySide2')) Module('scipy') Module('sphinx') Module('vtk', attr='VTK_VERSION')
[docs]class External(Software): """Register for external application version detection rules. This class holds a register of version detection rules for installed external applications. Each instance holds the rule for one application, and it is automatically registered at instantiation. The applications used by pyFormex are declared in this module, but users can add their own just by creating an External instance. Parameters ---------- name: str The application name as known in pyFormex: this is often the same as the executable name, but can be different if the executable name is complex. We try to use simple lower case names in pyFormex. command: str The command to run the application. Usually this includes an option to make the application just report its version and then exit. The command should be directly executable as-is, without invoking a new shell. If a shell is required, it should be made part of the command (see e.g. tetgen). Do not use commands that take a long time to load and run. regex: r-string A regular expression that extracts the version from the output of the command. If the application does not have or report a version, any non-empty string is accepted as a positive detection (for example the executable's name in a bin path). The regex string should contain one set of grouping parentheses, delimiting the part of the output that will be stored as version. If the output of the command does not match, an empty string is stored. Examples -------- >>> External.dict.clear() >>> External.detect_all() >>> External.print_all() """ def __init__(self, name, command, regex): """Create a registered external executable detection rule""" super().__init__(name) self.command = command self.regex = regex
[docs] def detect(self, fatal=False, quiet=False): """Detect the version of the external. Parameters ---------- fatal: bool, optional If True and the external can not be run, a fatal exception is raised. Default is to silently ignore the problem and return an empty version string. quiet: bool, optional If True, the whole operation is done silently. The only information about failure will be the returned empty version string. Returns ------- str The version string of the external, empty if the external can not be run. Notes ----- As a side effect, the detected version string is stored for later reuse. Thus subsequent tests will not try to re-detect. """ if pf.debugon(pf.DEBUG.DETECT): print(f"Check {self.name}\n{self.command}") P = pf.process.run(self.command) if pf.debugon(pf.DEBUG.DETECT): print(f"returncode: {P.returncode}\n" f"stdout:\n{P.stdout}\nstderr:\n{P.stderr}") version = '' # Some programs write their version to stderr, others to stdout # So we have to try both m = None if P.stdout: m = re.match(self.regex, P.stdout) if m is None and P.stderr: m = re.match(self.regex, P.stderr) if m: version = str(m.group(1)) super().detect(version, fatal) return self.version
External('python', 'python3 --version', r'Python (\S+)') External('admesh', 'admesh --version', r'ADMesh - version (\S+)') External('calculix', "ccx -v|tr '\n' ' '", r'[\n]*.*ersion (\S+)') External('calix', 'calix --version', r'CALIX-(\S+)') External('calpy', 'calpy3 --version', r'calpy (\S+)') External('dxfparser', 'pyformex-dxfparser --version', r'dxfparser (\S+)') External('ffmpeg', 'ffmpeg -version', r'[fF][fF]mpeg version (\S+)') External('freetype', 'freetype-config --ftversion', r'(\S+)') External('gts-bin', 'gts2stl -h', r'Usage: (gts)') External('gts-extra', 'pyformex-inside -h', r'Usage: (pyformex-inside)') External('imagemagick', 'import -version', r'Version: ImageMagick (\S+)') External('instant-meshes', 'instant-meshes -h', r'Syntax: (instant-meshes)') External('postabq', 'pyformex-postabq -V', r'postabq (\S+).*') External('recordmydesktop', 'recordmydesktop --version', r'recordMyDesktop v(\S+)') External('sphinx-build', "sphinx-build --version", r'sphinx-build (\S+)') External('tetgen', "bash -c 'type -p tetgen'", r'\S+(tetgen)') External('units', 'units --version', r'GNU Units version (\S+)') External('zip', 'zip -h', r'.*\nZip (\S+)')
[docs]def listLibraries(): """Return a list with the acceleration libraries""" return [m.__name__ for m in pf.lib.accelerated]
[docs]def listShaders(): """Return a list with the available GPU shader programs. Returns ------- list A list of the shader versions available. Notes ----- Shader programs are stored in the pyformex/glsl directory and consist of at least of two files: 'vertex_shader_SHADER.c' and 'fragment_shader_SHADER.c'. The SHADER part is the version mnemomic which can be used in the '--shader SHADER' option of the pyformex command. """ files = pf.cfg['shaderdir'].listTree( includefile=['vertex_shader_.*[.]c$', 'fragment_shader_.*[.]c$']) files = [f.name for f in files] vshaders = [f[14:-2] for f in files if f.startswith('v')] fshaders = [f[16:-2] for f in files if f.startswith('f')] shaders = set(vshaders) & set(fshaders) return sorted(shaders)
System = None
[docs]def detectSystem(): """Detect the system versions""" global System if System is None: system, host, release, version, arch = os.uname() System = { 'pyFormex_version': pf.__version__.split()[0], 'pyFormex_installtype': pf._installtype, 'pyFormex_fullversion': pf.fullversion(), 'pyFormex_libraries': ', '.join(listLibraries()), 'pyFormex_shaders': ', '.join(listShaders()), 'Python_version': sys.version.split()[0], 'Python_fullversion': sys.version.replace('\n', ' '), 'System': system, 'Host': host, 'Release': release, 'Version': version, 'Arch': arch, } if pf.GUI: System['Qt bindings'] = pf.gui.bindings return System
[docs]def detectedSoftware(probe=True, **kargs): """Detect software and system parameters. Parameters ---------- **kargs: Keyword arguments specifying which components to detect. Currently, the recognized arguments are: - System: detect system software including Python and pyFormex, - Module: detect Python modules used by pyFormex, - External: detect software used by pyFormex as external commands. The value of the parameter specifies which parts of that component are detected. In all cases, a value of 'all' means detect all known parts of the component. For 'Module' and 'External' a list of part names or a dict with part names as keys may be given, and only those parts will be detected. Available part names can be found from :meth:`Module.dict.keys()` or :meth:`External.dict.keys()`. probe: bool If True, detects all the requested software components. If False, returns the current detection state of the requested softwares. Returns ------- dict A dict with one or more of the keys 'System', 'Module' and 'External', each having a dict as value: - System: contains information about system, pyFormex, Python - Module: the detected Python modules - External: the detected external programs """ soft = {} if kargs.get('System', False): soft['System'] = detectSystem() # TODO: change the names to 'Module', 'External' ?? # TODO: do analoguous detection for System ?? for comp in (Module, External): name = comp.__name__ if parts := kargs.get(name, []): if parts == 'all': parts = None elif isinstance(parts, dict): parts = list(parts.keys()) elif isinstance(parts, (list, tuple)): pass else: raise ValueError("Invalid argument value") soft[name] = comp.detected(parts, probe=probe) return soft
[docs]def reportSoftware(soft='all', probe=True, header=None, sort=False, color=False): """Create a report about a software collection Parameters ---------- soft: dict | 'all' | 'detected' The software collection to be reported. This is either a dict as returned by detectedSoftware, or 'all' to report the already detected software or 'probe' to probe all registered software. probe: bool If True, detects all the requested software components. If False, reports the current detection state of the requested softwares. """ notprobed = '-- Not probed --' notfound = '** Not found **' ok = pf.color.ansi.fg(0, 128, 0) ok1 = pf.color.ansi.reset_all if color: notfound = pf.color.ansi.fg(255, 0, 0) + notfound + pf.color.ansi.reset_all else: ok = ok1 = '' def format_dict(d, sort): keys = sorted(d.keys()) if sort else d items = [ f" {k}: {ok+d[k]+ok1 if d[k] else notfound if d[k]=='' else notprobed}" for k in keys ] return '\n'.join(items) if soft in ['all', 'detected']: soft = { 'System': 'all', 'Module': 'all', 'External': 'all', } soft = detectedSoftware(**soft, probe=probe) s = "" if header: header = str(header) s += pf.utils.underlineHeader(header) for key, desc, srt in [ ('System', 'Installed System', False), ('Module', 'Python Modules', sort), ('External', 'External Programs', sort) ]: s += f"\n{desc}:\n" s += format_dict(soft[key], srt) s += '\n' return s
comparators = { '==': operator.__eq__, '!=': operator.__ne__, '>': operator.__gt__, '>=': operator.__ge__, '<': operator.__lt__, '<=': operator.__le__, } _re_Required = re.compile(r'(?P<cmp>(==|!=|([<>]=?)))? *(?P<require>.*)')
[docs]def compareVersion(has, want): """Check whether a detected version matches the requirements. Parameters ---------- has: str The version that is to be checked wanted: str or list/tuple of str One or more version strings to which ``has`` should be compared. Each version string can be preceded by one of the comparison operators. If no comparison operator is specified, '==' is used. List and tuples are handled recursively and can thus be nested. If a list, the result is True if any of its items gives a match. If a tuple, all items in the tuple must produce a match. Note that any tail behind x.y.z version is considered to be later version than x.y.z. Returns ------- bool: The result of the comparison. Examples -------- >>> compareVersion('2.7', '2.4.3') False >>> compareVersion('2.7', '>2.4.3') True >>> compareVersion('2.7', '>= 2.4.3') True >>> compareVersion('2.7', '>= 2.7-rc3') False >>> compareVersion('2.7-rc4', '>= 2.7-rc3') True >>> compareVersion('2.7', ('>= 2.4.3', '< 3.0')) True >>> compareVersion('3.5', [('>= 2.4.3', '< 3.0'), '3.5']) True """ if not has: return False if not want: return True elif isinstance(want, tuple): return all(compareVersion(has, i) for i in want) elif isinstance(want, list): return any(compareVersion(has, i) for i in want) m = _re_Required.match(want) if not m: return False d = m.groupdict() want = d['require'] comp = d['cmp'] if comp is None: comp = '==' return comparators[comp](Version(has), Version(want))
def checkItem(has, want): if compareVersion(has, want): return 'OK' else: return 'FAIL'
[docs]def checkDict(has, want): """Check that software dict has has the versions required in want""" return [(k, has[k], want[k], checkItem(has[k], want[k])) for k in want]
[docs]def checkSoftware(req, report=False): """Check that we have the matching components Returns True or False. If report=True, also returns a string with a full report. """ soft = detectedSoftware(**req) comp = [] for k in req: comp.extend(checkDict(soft[k], req[k])) result = all([s[3] == 'OK' for s in comp]) fmt = "%30s %15s %15s %10s\n" if report: s = pf.utils.underlineHeader(fmt % ("Item", "Found", "Required", "OK?")) for item in comp: s += fmt % item s += f"RESULT={'OK' if result else 'FAIL'}" return result, s else: return result
[docs]def registerSoftware(req): """Register the current values of required software""" soft = detectedSoftware(**req) reg = dict() for k in req: reg[k] = pf.utils.selectDict(soft[k], list(req[k].keys())) return reg
[docs]def formatDict(d, *, indent=2, sort_keys=False, mode='pprint'): """Format a possibly nested dict >>> d = {'a':0, 'd':{'y':'s', 'x':'t'}, 'b':1} >>> print(formatDict(d, mode='pprint')) {'a': 0, 'd': {'y': 's', 'x': 't'}, 'b': 1} >>> print(formatDict(d, mode='json')) { "a": 0, "d": { "y": "s", "x": "t" }, "b": 1 } >>> print(formatDict(d, mode='python')) {'a': 0, 'd': {'y': 's', 'x': 't'}, 'b': 1} >>> d = dict((('a'*i, i) for i in range(8))) >>> print(d) {'': 0, 'a': 1, 'aa': 2, 'aaa': 3, 'aaaa': 4, 'aaaaa': 5, 'aaaaaa': 6,\ 'aaaaaaa': 7} >>> print(formatDict(d, mode='pprint')) { '': 0, 'a': 1, 'aa': 2, 'aaa': 3, 'aaaa': 4, 'aaaaa': 5, 'aaaaaa': 6, 'aaaaaaa': 7} """ if mode == 'pprint': from pprint import pformat return pformat(d, indent=indent, sort_dicts=sort_keys) elif mode == 'json': import json return json.dumps(d, indent=indent, sort_keys=False) else: return f"{d!r}"
[docs]def storeSoftware(soft, fn, mode='json', indent=2): """Store the software collection on file. The collection is by default stored in JSON format. """ with open(fn, 'w') as fil: fil.write(formatDict(soft, indent=indent, mode=mode)) fil.write('\n')
[docs]def readSoftware(fn): """Read the software collection from file. Default mode is 'python' because it reads json as well as python. 'legacy' can be used to read old software files, though it is recommended to change the files by removing the 'soft = ' at the start. """ with open(fn, 'r') as fil: txt = fil.read() if txt.startswith('soft'): # This is probably a legacy format txt = txt.split('=')[1] soft = eval(txt) return soft
#### execute as pyFormex script for testing ######## if __name__ in ["__draw__", "__script__"]: pg = pf.gui.guicore Required = { 'System': { 'pyFormex_installtype': 'R', 'Python_version': ('>= 3.9.0', '<3.12.0'), }, 'Module': { 'pyformex': '>= 2.7', 'matplotlib': '1.1.1', 'numpy': '1.16', }, 'External': { 'admesh': '>= 0.95', }, } with pg.busyCursor(): soft = detectedSoftware() print((reportSoftware(header="Detected Software", color=False))) print('\n ') print((reportSoftware(Required, header="Required Software", color=False))) print('\n ') print('CHECK') ok, report = checkSoftware(Required, True) print(report) reg = registerSoftware(Required) print("REGISTER") print(formatDict(reg)) storeSoftware(reg, 'checksoft.json') req = readSoftware('checksoft.json') print('CHECK REGISTERED') print(formatDict(req)) ok, report = checkSoftware(req, True) print(report) # End