#
##
## This file is part of pyFormex 2.0 (Mon Sep 14 12:29:05 CEST 2020)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: http://pyformex.org
## Project page: http://savannah.nongnu.org/projects/pyformex/
## Copyright 2004-2020 (C) Benedict Verhegghe (benedict.verhegghe@ugent.be)
## 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 re
import operator
from distutils.version import LooseVersion as SaneVersion
from collections import OrderedDict
from importlib import import_module
import pyformex as pf
from pyformex import process
[docs]class Software():
"""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.
The default software object only has two attributes:
Attributes
----------
name: str
The registered name of the software.
version: str
The version of the software. This is set to None when the name is
registered, and becomes a (possibly empty) string after calling
the :meth:`detect` method.
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):
ValueError: foo is not a registered Software
>>> foo = Software('foo')
>>> Software.require('foo')
Traceback (most recent call last):
ValueError: Required Software 'foo' (foo) not found
>>> Software.print_all()
numpy (detected)
foo (** Not Found **)
>>> Software('foo')
Traceback (most recent call last):
ValueError: A Software with name 'foo' is already registered
"""
register = OrderedDict()
def __init__(self, name):
"""Create a registered software name"""
if name in Software.register:
raise ValueError(f"A {self.__class__.__name__} "
f"with name '{name}' is already registered")
self.name = name
self.version = None
self.__class__.register[name] = self
[docs] 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
[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:
pf.debug(f"Congratulations! You have {self.name} ({version})",
pf.DEBUG.DETECT)
else:
if not fatal:
pf.debug(f"ALAS! I could not find {self.__class__.__name__} "
f"'{self.name}' on your system", pf.DEBUG.DETECT)
if fatal:
pf.error("Sorry, I'm getting out of here....")
sys.exit()
self.version = version
return self.version
[docs] @classmethod
def detect_all(clas):
"""Detect all registered softwares.
Usually, the detection is only performed when needed.
Calling this method will perform the detection for all
registered softwares.
"""
for name in clas.register:
clas.register[name].detect()
[docs] @classmethod
def print_all(clas):
"""Print the list of registered softwares"""
for name in clas.register:
version = clas.register[name].version
if not version:
version = '** Not Found **'
print(f" {name} ({version})")
[docs] @classmethod
def detected(clas, all=False):
"""Return the successfully detected softwares and their version
Returns
-------
OrderedDict
A dict with the software name as key and the detected version
as value.
"""
return OrderedDict([(k, clas.register[k].version) for k in
clas.register if all or clas.register[k].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.register:
raise ValueError(f"{name} is not a registered {clas.__name__}")
if check or clas.register[name].version is None:
clas.register[name].detect(fatal=fatal, quiet=quiet)
return clas.register[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.register[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)
raise ValueError(f"Required {clas.__name__} '{name}' ({realname}) not found")
else:
if version is not None:
if not compareVersion(ver, version):
realname = clas.register[name].name
errmsg = f"""..
**{clas.__name__} version {name} ({version}) does not meet requirements ({version})!**
You activated some functionality requiring {clas.__name__} '{realname}'.
However, the required version for that software could not be found.
"""
pf.error(errmsg)
raise ValueError(f"Required version of {clas.__name__} "
f"'{name}' ({realname}) not found")
[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.
Examples
--------
>>> Module.register.clear()
>>> Module.detect_all()
>>> Module.print_all()
>>> np = Module('numpy')
>>> pil = Module('pil', modname='PIL', attr='VERSION')
>>> Module.print_all()
numpy (** Not Found **)
pil (** Not Found **)
>>> Module.has('numpy')
'1.16.2'
>>> Module.print_all()
numpy (1.16.2)
pil (** Not Found **)
>>> Module.has('foo')
Traceback (most recent call last):
ValueError: foo is not a registered Module
>>> Module.require('foo')
Traceback (most recent call last):
ValueError: foo is not a registered Module
>>> foo = Module('foo','FooBar')
>>> Module.has('foo')
''
>>> Module.require('foo')
Traceback (most recent call last):
ValueError: Required Module 'foo' (FooBar) not found
Now fake a detection of Module 'foo'
>>> Module.register['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):
ValueError: Required version of Module 'foo' (FooBar) not found
"""
register = OrderedDict()
def __init__(self, name, modname=None, attr=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
[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:
pf.debug(self.name, pf.DEBUG.DETECT)
m = import_module(self.name)
pf.debug(m, pf.DEBUG.DETECT)
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 = import_module(self.attr[0])
pf.debug(m, pf.DEBUG.DETECT)
ver = self.attr[1:]
for a in ver:
m = getattr(m, a)
pf.debug(m, pf.DEBUG.DETECT)
except Exception:
# failure: unexisting or unregistered modules
if fatal:
raise
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('calpy')
Module('docutils')
Module('gl2ps', attr='GL2PS_VERSION')
Module('gnuplot', modname='Gnuplot')
# Module('ipython', modname='IPython',)
# Module('ipython-qt', modname='IPython.frontend.qt',)
Module('matplotlib')
Module('numpy')
Module('pil', modname='PIL')
Module('pydicom')
Module('pyformex')
Module('pyopengl', modname='OpenGL')
Module('pyqt4', modname='PyQt4.QtCore', attr='QT_VERSION_STR')
Module('pyqt4gl', modname='PyQt4.QtOpenGL', attr=('PyQt4.QtCore', 'QT_VERSION_STR'))
Module('pyqt5', modname='PyQt5.QtCore', attr='PYQT_VERSION_STR')
Module('pyqt5gl', modname='PyQt5.QtOpenGL', attr=('PyQt5.QtCore', 'PYQT_VERSION_STR'))
Module('pyside', modname='PySide')
Module('pyside2', modname='PySide2')
Module('scipy')
Module('vtk', modname='pyformex.vtk_light', 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.register.clear()
>>> External.detect_all()
>>> External.print_all()
"""
register = OrderedDict()
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.
"""
pf.debug(f"Check {self.name}\n{self.command}", pf.DEBUG.DETECT)
P = process.run(self.command)
pf.debug(f"returncode: {P.returncode}\n"
f"stdout:\n{P.stdout}\nstderr:\n{P.stderr}",
pf.DEBUG.DETECT)
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('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', 'gtsset -h', r'Usage(:) ')
External('gts-bin', 'gts2stl -h', r'Usage(:) ')
External('gts-extra', 'gtsinside -h', r'Usage(:) ')
External('imagemagick', 'import -version', r'Version: ImageMagick (\S+)')
External('postabq', 'pyformex-postabq -V', r'postabq (\S+).*')
External('python', 'python3 --version', r'Python (\S+)')
External('recordmydesktop', 'recordmydesktop --version', r'recordMyDesktop v(\S+)')
External('tetgen', "bash -c 'type -p tetgen'", r'(\S+)')
External('units', 'units --version', r'GNU Units version (\S+)')
External('vmtk', "bash -c 'type -p vmtk'", r'(\S+)')
def Libraries():
from pyformex import lib
return [m.__name__ for m in lib.accelerated]
[docs]def Shaders():
"""Return a list of the available GPU shader programs.
Shader programs are in the pyformex/glsl directory and consist
at least of two files:
'vertex_shader_SHADER.c' and 'fragment_shader_SHADER.c'.
This function will return a list of all the SHADER filename parts
currently available. The default shader programs do not have the
'_SHADER' part and will not be contained in this list.
"""
files = pf.cfg['shaderdir'].listTree(
listdirs=False, sorted=True,
includefiles=['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)
[docs]def detectedSoftware(all=True):
"""Return a dict with all detected helper software"""
Module.detect_all()
External.detect_all()
system, host, release, version, arch = os.uname()
System = OrderedDict([
('pyFormex_version', pf.__version__.split()[0]),
('pyFormex_installtype', pf.installtype),
('pyFormex_fullversion', pf.fullVersion()),
('pyFormex_libraries', ', '.join(Libraries())),
('pyFormex_shaders', ', '.join(Shaders())),
('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
soft = {
'System': System,
'Modules': Module.detected(all),
'Externals': External.detected(all),
}
return soft
def reportSoftware(soft=None, header=None):
from pyformex import utils
notfound = '** Not Found **'
def format_dict(d, sort=True):
keys = sorted(d.keys()) if sort else d
items = [f" {k} ({d[k] if d[k] else notfound})" for k in keys]
return '\n'.join(items)
if soft is None:
soft = detectedSoftware()
s = ""
if header:
header = str(header)
s += utils.underlineHeader(header)
for key, desc, sort in [
('System', 'Installed System', False),
('Modules', 'Detected Python Modules', True),
('Externals', 'Detected External Programs', True)
]:
s += f"\n{desc}:\n"
s += format_dict(soft[key], sort=sort)
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.
has is the version string detected.
want is the required version string, possibly preceded by one
of the doubly underscored comparison operators: __gt__, etc.
If no comparison operator is specified, '__eq__' is assumed.
Note that any tail behind x.y.z version is considered to be later
version than x.y.z.
Returns the result of the comparison: True or False
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
"""
if not has:
return False
m = re_Required.match(want)
if not m:
return False
d = m.groupdict()
want = d['require']
comp = d['cmp']
if comp is None:
comp = '=='
has = SaneVersion(has)
want = SaneVersion(want)
return comparators[comp](has, 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.
"""
from pyformex import utils
soft = detectedSoftware()
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 = 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"""
from pyformex import utils
soft = detectedSoftware()
reg = {}
for k in req:
reg[k] = utils.selectDict(soft[k], list(req[k].keys()))
return reg
[docs]def soft2config(soft):
"""Convert software collection to config"""
from pyformex import utils
from pyformex import config
conf = config.Config()
for k in soft:
conf.update(utils.prefixDict(soft[k], k+'/'))
return conf
[docs]def config2soft(conf):
"""Convert software collection from config"""
from pyformex import utils
soft = {}
for k in ['System', 'Modules', 'Externals']:
soft[k] = utils.subDict(conf, prefix=k+'/')
return soft
[docs]def storeSoftware(soft, fn, mode='pretty'):
"""Store the software collection on file."""
if mode == 'pretty':
with open(fn, 'w') as fil:
fil.write("soft = "+formatDict(soft)+'\n')
elif mode == 'python':
with open(fn, 'w') as fil:
fil.write("soft=%r\n" % soft)
elif mode == 'config':
conf = soft2config(soft)
conf.write(fn)
elif mode == 'pickle':
import pickle
print(("PICKLING", soft))
pickle.dump(soft, open(fn, 'w'))
[docs]def readSoftware(fn, mode='python'):
"""Read the software collection from file.
- `mode` = 'pretty': readable, editable
- `mode` = 'python': readable, editable
- `mode` = 'config': readable, editable
- `mode` = 'pickle': binary
"""
from pyformex import config
from pyformex import utils
soft = None
if mode == 'python':
d = {}
utils.execFile(fn, {}, d)
soft = d['soft']
elif mode == 'config':
conf = config.Config(fn)
soft = config2soft(conf)
elif mode == 'pickle':
import pickle
soft = pickle.load(open(fn, 'r'))
return soft
#### execute as pyFormex script for testing ########
if __name__ in ["_draw__", "__script__"]:
Required = {
'System': {
'pyFormex_installtype': 'R',
'Python_version': '>= 3.6.0',
},
'Modules': {
'pyformex': '>= 0.9.1',
'matplotlib': '1.1.1',
},
'Externals': {
'admesh': '>= 0.95',
},
}
soft = detectedSoftware()
print((reportSoftware(header="Detected Software")))
print('\n ')
print((reportSoftware(Required, header="Required Software")))
print('\n ')
print('CHECK')
ok, report = checkSoftware(Required, True)
print(report)
reg = registerSoftware(Required)
print("REGISTER")
print(formatDict(reg))
storeSoftware(reg, 'checksoft.py')
req = readSoftware('checksoft.py')
print('CHECK REGISTERED')
print(formatDict(req))
ok, report = checkSoftware(req, True)
print(report)
# End