Source code for software

#
##
##  This file is part of pyFormex 1.0.7  (Mon Jun 17 12:20:39 CEST 2019)
##  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-2019 (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/.
##
"""software.py

A module to help with detecting required software and helper software,
and to check the versions of it.

This module is currently experimental. It contains some old functions
moved here from utils.py.
"""
from __future__ import absolute_import, division, print_function

import os
import sys
import re
from distutils.version import LooseVersion as SaneVersion
from collections import OrderedDict

import pyformex as pf
from pyformex import Path

# Python modules we know how to use
# Do not include pyformex or python here: they are predefined
# and could be erased by the detection
# The value is a sequence of:
#   - module name
#   - module name to load the version
#   - a sequence of attributes to get the version
# If empty, the attribute is supposed to be '__version__'
# If module name is an empty string, it is supposed to be equal to our alias
known_modules = {
    'calpy': (),
#    'dicom': (),    # we no longer support it
    'docutils': (),
#    'gdcm': ('', '', 'GDCM_VERSION'),  # not in buster, not working in Python3
    'gl2ps': ('', '', 'GL2PS_VERSION'),
    'gnuplot': ('Gnuplot',),
#    'ipython': ('IPython',),
#    'ipython-qt': ('IPython.frontend.qt',),
    'matplotlib': (),
    'numpy': (),
    'pil': ('PIL', 'PIL', 'VERSION'),
    'pydicom': (),
    'pyopengl': ('OpenGL',),
    'pyqt4': ('PyQt4.QtCore', 'PyQt4', 'QtCore', 'QT_VERSION_STR'),
    'pyqt4gl': ('PyQt4.QtOpenGL', 'PyQt4', 'QtCore', 'QT_VERSION_STR'),
    'pyside': ('PySide',),
    'pyside2': ('PySide2',),
    'scipy': (),
    'vtk': ('pyformex.vtk_light', 'pyformex', 'vtk_light', 'VTK_VERSION'),
     }

known_externals = {
# BEWARE ! Because utils.system now does not use a new shell by default,
# commands that require a shell should spawn the shell with the command
# as a -c parameter (see e.g. tetgen)

#  NOTE: abaqus command may hang longtime on checking the license server
#    'abaqus': ('abaqus info=sys|head -n2|tail -n1', 'Abaqus (\S+)'),
    'admesh': ('admesh --version', r'ADMesh - version (\S+)'),
    'calculix': ('ccx -v', r'.*version (\S+)'),
    'calix': ('calix --version', r'CALIX-(\S+)'),
    'calpy': ('calpy --version', r'Calpy (\S+)'),
    'dxfparser': ('pyformex-dxfparser --version', r'dxfparser (\S+)'),
    'ffmpeg': ('ffmpeg -version', r'[fF][fF]mpeg version (\S+)'),
    'freetype': ('freetype-config --ftversion', r'(\S+)'),
    'gts': ('gtsset -h', r'Usage(:) '),
    'gts-bin': ('gts2stl -h', r'Usage(:) '),
    'gts-extra': ('gtsinside -h', r'Usage(:) '),
    'imagemagick': ('import -version', r'Version: ImageMagick (\S+)'),
    'postabq': ('pyformex-postabq -V', r'postabq (\S+).*'),
    'python': ('python --version', r'Python (\\S+)'),
    'recordmydesktop': ('recordmydesktop --version', r'recordMyDesktop v(\S+)'),
    'tetgen': ("bash -c 'type -p tetgen'", r'(\S+)'),
    'units': ('units --version', r'GNU Units version (\S+)'),
    'vmtk': ("bash -c 'type -p vmtk'", r'(\S+)'),
    }


if pf.PY3:
    # Some detect commands fail on Python3
    del known_externals['calculix']
    del known_externals['postabq']
    del known_externals['recordmydesktop']


# versions of detected modules
the_version = {
    'pyformex': pf.__version__.split()[0],
    'python': sys.version.split()[0],
    }
# versions of detected external commands
the_external = {}

[docs]def checkVersion(name, version, external=False): """Checks a version of a program/module. name is either a module or an external program whose availability has been registered. Default is to treat name as a module. Add external=True for a program. Return value is -1, 0 or 1, depending on a version found that is <, == or > than the requested values. This should normally understand version numbers in the format 2.10.1 Returns -2 if no version found. """ if external: ver = hasExternal(name) else: ver = hasModule(name) if not ver: return -2 if SaneVersion(ver) > SaneVersion(version): return 1 elif SaneVersion(ver) == SaneVersion(version): return 0 else: return -1
[docs]def hasModule(name, check=False): """Test if we have the named module available. Returns a nonzero (version) string if the module is available, or an empty string if it is not. By default, the module is only checked on the first call. The result is remembered in the the_version dict. The optional argument check==True forces a new detection. """ if name in the_version and not check: return the_version[name] else: return checkModule(name)
[docs]def requireModule(name, version=None, comp='ge'): """Ensure that the named Python module/version is available. Checks that the specified module is available, and that its version number is not lower than the specified version. If no version is specified, any version is ok. The default comparison operator 'ge' can be replaced with one of: 'eq', 'ge', 'gt', 'le', 'lt', 'ne'. Returns if the required module/version could be loaded, else an error is raised. """ if pf.sphinx: # Do not check when building docs return import operator ver = hasModule(name) if not ver: if name in known_modules: # Get the correct name, if different from our alias try: realname = known_modules[name][0] if realname: name = realname except: pass attr = 'required' else: attr = 'unknown' errmsg = "Could not load %s module '%s'" % (attr, name) errmsg = """.. **Module %s not found!** You activated some functionality requiring the Python module '%s'. However, the module '%s' could not be loaded. Probably it is not installed on your system. """ % (name, name, name) pf.error(errmsg) raise ValueError(errmsg) else: if version is not None: op = getattr(operator, comp) if not op(checkVersion(name, version), 0): errmsg = """.. **Version of module %s (%s) does not meet requirements (%s %s)!** You activated some functionality using the Python module '%s'. However, the required version for that module could not be loaded. """ % (name, ver, comp, version, name) pf.error(errmsg) raise ValueError(errmsg)
[docs]def checkAllModules(): """Check the existence of all known modules. """ [checkModule(n, quiet=True) for n in known_modules]
[docs]def checkModule(name, ver=(), fatal=False, quiet=False): """Check if the named Python module is available, and record its version. ver is a tuple of: - modname: name of the module to test import - vername: name of the module holding the version string - more fields are consecutive attributes leading to the version string The obtained version string is returned, empty if the module could not be loaded. The (name,version) pair is also inserted into the the_version dict. If fatal=True, pyFormex will abort if the module can not be loaded. """ if len(ver) == 0 and name in known_modules: ver = known_modules[name] modname = name if len(ver) > 0 and len(ver[0]) > 0: modname = ver[0] try: if not quiet: pf.debug(modname, pf.DEBUG.DETECT) m = __import__(modname) if not quiet: pf.debug(m, pf.DEBUG.DETECT) if len(ver) > 1 and len(ver[1]) > 0: modname = ver[1] m = __import__(modname) if not quiet: pf.debug(m, pf.DEBUG.DETECT) ver = ver[2:] if len(ver) == 0: ver = ('__version__',) for a in ver: m = getattr(m, a) if not quiet: pf.debug(m, pf.DEBUG.DETECT) except: # 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!) m = str(m) _congratulations(name, m, 'module', fatal, quiet=quiet) the_version[name] = m return m
[docs]def hasExternal(name, force=False): """Test if we have the external command 'name' available. Returns a nonzero string if the command is available, or an empty string if it is not. The external command is only checked on the first call. The result is remembered in the the_external dict. """ if name in the_external and not force: return the_external[name] else: return checkExternal(name)
[docs]def requireExternal(name): """Ensure that the named external program is available. If the module is not available, an error is raised. """ if pf.sphinx: # Do not require externals when building docs return if not hasExternal(name): if name in known_externals: # Get the correct name, if different from our alias try: realname = known_externals[name][0] if realname: name = realname except: pass attr = 'required' else: attr = 'unknown' errmsg = """.. **Program %s not found!** You activated some functionality requiring the external program '%s'. However, '%s' was not found on your system. """ % (name, name, name) pf.error(errmsg) raise ValueError(errmsg)
[docs]def checkAllExternals(): """Check the existence of all known externals. Returns a dict with all the known externals, detected or not. The detected ones have a non-zero value, usually the version number. """ [checkExternal(n, quiet=True) for n in known_externals] return the_external
[docs]def checkExternal(name, command=None, answer=None, quiet=False): """Check if the named external command is available on the system. name is the generic command name, command is the command as it will be executed to check its operation, answer is a regular expression to match positive answers from the command. answer should contain at least one group. In case of a match, the contents of the match will be stored in the the_external dict with name as the key. If the result does not match the specified answer, an empty value is inserted. Usually, command will contain an option to display the version, and the answer re contains a group to select the version string from the result. As a convenience, we provide a list of predeclared external commands, that can be checked by their name alone. """ from pyformex import utils if command is None or answer is None: cmd, ans = known_externals.get(name, (name, r'(.+)\n')) if command is None: command = cmd if answer is None: answer = ans pf.debug("Check %s\n%s" % (name, command), pf.DEBUG.DETECT) P = utils.system(command) pf.debug("Status:\n%s\nStdout:\n%s\nStderr:\n%s" % (P.sta, P.out, P.err), pf.DEBUG.DETECT) version = '' # Beware: some programs write their version to stderr, others to stdout m = None if P.out: m = re.match(answer, P.out) if m is None and P.err: m = re.match(answer, P.err) if m: version = str(m.group(1)) _congratulations(name, version, 'program', quiet=quiet) the_external[name] = version return str(version)
def _congratulations(name, version, typ='module', fatal=False, quiet=False, severity=2): """Report a detected module/program.""" if version: if not quiet: pf.debug("Congratulations! You have %s (%s)" % (name, version), pf.DEBUG.DETECT) else: if not quiet or fatal: pf.debug("ALAS! I could not find %s '%s' on your system" % (typ, name), pf.DEBUG.DETECT) if fatal: pf.error("Sorry, I'm getting out of here....") sys.exit() def Libraries(): from pyformex.lib import accelerated return [m.__name__ for m in 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. """ from pyformex import utils 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""" if all: checkAllModules() checkAllExternals() system, host, release, version, arch = os.uname() soft = { 'System': OrderedDict([ ('pyFormex_version', the_version['pyformex']), ('pyFormex_installtype', pf.installtype), ('pyFormex_fullversion', pf.fullVersion()), ('pyFormex_libraries', ', '.join(Libraries())), ('pyFormex_shaders', ', '.join(Shaders())), ('Python_version', the_version['python']), ('Python_fullversion', sys.version.replace('\n', ' ')), ('System', system), ('Host', host), ('Release', release), ('Version', version), ('Arch', arch), ]), 'Modules': the_version, 'Externals': the_external, } 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 = [" {} ({})".format(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 += "\n%s:\n" % desc s += format_dict(soft[key], sort=sort) s += '\n' return s def fmtDict(d, name=None): s = str(d) out = '' if name: out += name out += ' = ' i = j = ind = 0 while j < len(s): print(i, j, s[i:j+1]) if s[j] == '{': out += s[i:j+1] ind += 4 out += '\n' + ' '*ind i = j+1 j = j + 1 return out
[docs]def formatDict(d, indent=4): """Format a dict in nicely formatted Python source representation. Each (key,value) pair is formatted on a line of the form:: key = value If all the keys are strings containing only characters that are allowed in Python variable names, the resulting text is a legal Python script to define the items in the dict. It can be stored on a file and executed. This format is the storage format of the Config class. """ s = "{\n" for k, v in d.items(): s += "%s'%s' : " % (' '*indent, k) if isinstance(v, dict): s += formatDict(v, indent+4) else: s += "%r" % v s += ",\n" s += "%s}" % (' '*indent) return s
import operator comparators = { '==': operator.__eq__, '!=': operator.__ne__, '>': operator.__gt__, '>=': operator.__ge__, '<': operator.__lt__, '<=': operator.__le__, } re_Required = re.compile('(?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 has == want: return 'Matching' if not has: return 'Missing' if has and not want: return 'Unwanted' has = SaneVersion(has) want = SaneVersion(want) print(("HAS %s; WANT %s" % (has, want))) if has == want: return 'Matching' if has < want: return 'Too Old' if has > want: return 'Too Recent' def checkItem2(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], checkItem2(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])) print(comp) result = all([s[3] == 'OK' for s in comp]) if report: s = utils.underlineHeader("%30s %15s %15s %10s\n" % ("Item", "Found", "Required", "OK?")) for item in comp: s += "%30s %15s %15s %10s\n" % item s += "RESULT=%s\n" % ('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': from pyformex 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 #fn = Path(fn) if mode == 'python': with open(fn, 'r') as fil: exec(fil.read()) elif mode == 'config': conf = config.Config(fn) soft = config2soft(conf) elif mode == 'pickle': from pyformex 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', }, 'Modules': { 'pyformex': '>= 0.9.1', 'python': '>= 2.7.3', '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