Source code for cmdtools

#
##
##  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/.
##

# This is the only pyFormex module that is imported by the main script,
# so this is the place to put startup code

"""pyFormex command line tools

This module contains some command line tools that are run through the
pyformex command, but do not start a full pyFormex program: just execute
some small task and exit.

Furthermore it contains some functions for handling the user preferences.
"""

import sys
import os
import warnings

import pyformex as pf
from pyformex import Path
from pyformex import utils
from pyformex import software

# Note: This module and all of the above should NOT import numpy

###########################  main  ################################


[docs]def remove_pyFormex(pyformexdir, executable): """Remove the pyFormex installation. This will remove a pyFormex installation that was done using the 'python setup.py install' command from a source distribution in 'tar'gz' format. The procedure is interactive and will ask for confirmation. Parameters ---------- pyformexdir: Path Absolute Path to the pyFormex installation directory. executable: Path Path to the pyformex executable. Notes ----- This implements the `pyformex --remove` functionality. """ if not executable.stem == 'pyformex': print( """ ############ ERROR! The --remove option can only be executed from the pyformex command. Use the command: pyformex --remove. """ ) return if os.getcwd() == pyformexdir.parent: print(f"pyFormex loaded from: {pyformexdir}") print(f"Your current working directory: {os.getcwd()}") print( """ ############ ERROR! The --remove option can not be used when you're inside the directory containing the pyformex package. Go to another working directory first. """ ) return if pf.installtype == 'D': print( """ ############ ERROR! It looks like this version of pyFormex was installed from a distribution .deb package. You should use your distribution's package tools to remove this pyFormex installation. """ ) return if pf.installtype in 'SG': print( """ ############ ERROR! It looks like you are running pyFormex directly from a source tree at %s. I will not remove it. If you have enough privileges, you can just remove the whole source tree from the file system. """ % pyformexdir ) return if not pyformexdir.is_absolute(): print( """ ############ ERROR! The pyFormex installation path is not an absolute path. Probably you are executing this command from inside a pyFormex source directory. The 'pyformex --remove' command should be executed from outside the source. """ ) return # If we get here, this is probably an install from tarball release # Remove the installation import glob prefixdir = pyformexdir.commonpath('/usr/local') bindir = prefixdir / 'bin' egginfo = pyformexdir.with_name( "pyformex-%s.egg-info" % (pf.__version__.replace('-', '_')) ) print("egginfo = %s" % egginfo) script = bindir / 'pyformex' print("script = %s" % script) # This Python version this = '3' other = '' scripts = [script] + glob.glob(script + '-*') gtsexe = ['gtscoarsen', 'gtsinside', 'gtsrefine', 'gtsset', 'gtssmooth'] scripts += [bindir / g for g in gtsexe] scripts = [ Path(p) for p in scripts ] scripts = [ p for p in scripts if p.exists() ] datadir = prefixdir / 'share' data = [ datadir / 'man/man1' / f for f in [ 'pyformex.1', 'pyformex-dxfparser.1', 'pyformex-postabq.1', 'gtscoarsen.1', 'gtsinside.1', 'gtsrefine.1', 'gtsset.1', 'gtssmooth.1' ] ] data += [ datadir / f for f in [ 'applications/pyformex.desktop', 'pixmaps/pyformex-64x64.png', 'pixmaps/pyformex.xpm', ] ] data = [ Path(p) for p in data ] data = [ p for p in data if p.exists() ] other_files = scripts + data do_other = 'REMOVE' print( """ ############ BEWARE! This procedure will: - %s the complete Python%s pyFormex installation in %s, - %s the pyFormex executable(s) (%s) in %s, - %s some pyFormex data files (%s) in %s. You should only use this on a pyFormex installed from a source .tar.gz release with the command 'python3 setup.py install'. You need proper permissions to actually delete the files. After successful removal, you will not be able to run this pyFormex again, unless you re-install it. """ % ( 'REMOVE', this, pyformexdir, do_other, ', '.join( [p.name for p in scripts] ), bindir, do_other, ', '.join([p.name for p in data]), datadir ) ) s = input("Are you sure you want to remove pyFormex? yes/NO: ") if s == 'yes': print(f"Removing pyformex tree: {pyformexdir}") pyformexdir.removeTree() if egginfo.exists(): egginfo.unlink() if other == '': for f in other_files: print(f"Removing {f}") try: Path(f).unlink() except Exception: print(f"Could not remove {f}") print("\nBye, bye! I won't be back until you reinstall me!") elif s.startswith('y') or s.startswith('Y'): print("You need to type exactly 'yes' to remove me.") else: print("Thanks for letting me stay this time.") sys.exit()
[docs]def list_modules(pkgs=['all']): """Return the list of pure Python modules in a pyFormex subpackage. Parameters ---------- pkgs: list of str A list of pyFormex subpackage names. The subpackage name is a subdirectory of the main pyformex package directory. Two special package names are recognized: - 'core': returns the modules in the top level pyformex package - 'all': returns all pyFormex modules An empty list is interpreted as ['all']. Returns ------- list of str A list of all modules in the specified packages. Notes ----- This implements the ``pyformex --listmodules`` functionality. """ modules = [] if pkgs == []: pkgs = ['all'] for subpkg in pkgs: modules.extend(utils.moduleList(subpkg)) return modules
[docs]def run_pytest(modules): """Run the pytests for the specified pyFormex modules. Parameters ---------- modules: list of str A list of pyFormex modules in dotted Python notation, relative to the pyFormex package. If an empty list is supplied, all available pytests will be run. Notes ----- Test modules are stored under the path `pf.cfg['testdir']`, with the same hierarchy as the pyFormex source modules, and are named `test_MODULE.py`, where MODULE is the corresponding source module. This implements the `pyformex --pytest` functionality. """ try: import pytest except Exception: print("Can not import pytest") pass testpath = pf.cfg['testdir'] print(f"TESTPATH={testpath}") args = ['--maxfail', '10'] if not modules: pytest.main(args + [testpath]) else: print("Running pytests for modules %s" % modules) for m in modules: path = Path(*m.split('.')) path = testpath / path.with_name(f"test_{path.name}.py") if path.exists(): pytest.main(args + [path]) else: print("No such test module: %s" % path)
[docs]def run_doctest(modules): """Run the doctests for the specified pyFormex modules. Parameters ---------- modules: list of str A list of pyFormex modules in dotted Python notation, relative to the pyFormex package. If an empty list is supplied, all doctests in all pyFormex modules will be run. Notes ----- Doctests are tests embedded in the docstrings of the Python source. To allow consistent output of floats independent of machine precision, numpy's floating point print precision is set to two decimals. This implements the `pyformex --doctest` functionality. """ import numpy if software.Module.check('numpy', '< 1.14'): # does not have the legacy argument numpy.set_printoptions(precision=2, suppress=True) else: numpy.set_printoptions(precision=2, suppress=True, legacy='1.13') from pyformex import arraytools numpy.set_string_function(arraytools.array2str) # set verbosity to 0 pf.options.verbose = 0 os.chdir(pf.pyformexdir.parent) if not modules: modules = ['all'] todo = [] for mod in modules: if mod in ['all'] or mod.endswith('.'): todo.extend(utils.moduleList(mod.replace('.', ''))) else: todo.append(mod) # Temporary hack moving 'software' module to the end, # as it causes failures for some other modules m = 'software' if m in todo: todo.remove(m) todo.append(m) pf.debug(f"Final list of modules to test: {todo}", pf.DEBUG.DOCTEST) # Now perform the tests FAILED, failed, attempted = 0, 0, 0 for m in todo: try: result = doctest_module(m) failed += result.failed attempted += result.attempted except Exception as e: result = f"FAIL\n Failed because: {e}" FAILED += 1 print(f"Module {m}: {result}") if len(todo) > 1: print('-'*60) print(f"Totals: attempted={attempted} tests, failed={failed} tests, " f"FAILED={FAILED} modules")
[docs]def doctest_module(module): """Run the doctests in a single module's docstrings. All the doctests in the docstrings of the specified module will be run. Parameters ---------- module: str A pyFormex module in dotted path notation. The leading 'pyformex.' can be omitted. """ import doctest import importlib if not module.startswith('pyformex'): module = 'pyformex.' + module mod = importlib.import_module(module) pf.options.verbose = 0 with warnings.catch_warnings(): warnings.simplefilter("ignore") pf.debug(f"Running doctests on {mod}", pf.DEBUG.DOCTEST) return doctest.testmod( mod, optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS)
[docs]def migrateUserConfig(): """Migrate the user preferences in $HOME/.config Conversion of old style has been abandoned. Currently this does nothing. """ pass
def _sanitize_warnings_filters(c): """Sanitize the 'warnings/filters' setting. The setting should be a set (to avoid doubles) and not contain DeprecationWarnings. Returns the corrected setting, possibly None. """ if isinstance(c, set): # Accept as is pass elif isinstance(c, list): # Remove the obsolete filters c = set(f for f in c if isinstance(f, tuple) and not (len(f) > 2 and f[2] == 'D')) else: # Remove the setting c = None return c
[docs]def apply_config_changes(cfg): """Apply incompatible changes in the configuration cfg is the user configuration that is to be saved. """ # Safety checks # Warning filters if 'warnings' in cfg and 'filters' in cfg['warnings']: # We need to check if the cfg really owns the setting pf.debug('Sanitizing settings', pf.DEBUG.CONFIG) c = _sanitize_warnings_filters(cfg['warnings/filters']) cfg['warnings/filters'] = c # Path required for p in ('workdir', ): if p in cfg and not isinstance(cfg[p], Path): cfg[p] = Path(p) # Adhoc changes if isinstance(cfg['gui/dynazoom'], str): cfg['gui/dynazoom'] = [cfg['gui/dynazoom'], ''] for i in range(8): t = "render/light%s" % i try: cfg[t] = dict(cfg[t]) except Exception: pass for d in ['scriptdirs', 'appdirs']: if d in cfg: scriptdirs = [] for i in cfg[d]: if i[1] == '' or Path(i[1]).is_dir(): scriptdirs.append(tuple(i)) elif i[0] == '' or Path(i[0]).is_dir(): scriptdirs.append((i[1], i[0])) cfg[d] = scriptdirs # Rename settings for old, new in [ ('history', 'gui/scripthistory'), ('gui/history', 'gui/scripthistory'), ('raiseapploadexc', 'showapploaderrors'), ('webgl/xtkscript', 'webgl/script'), ]: if old in cfg: if new not in cfg: cfg[new] = cfg[old] del cfg[old] # Delete settings for key in [ 'input/timeout', 'filterwarnings', 'render/ambient', 'render/diffuse', 'render/specular', 'render/emission', 'render/material', 'canvas/propcolors', 'Save changes', 'canvas/bgmode', 'canvas/bgcolor2', '_save_', ]: if key in cfg: print("DELETING CONFIG VARIABLE %s" % key) del cfg[key]
def printcfg(key): try: print("!! refcfg[%s] = %s" % (key, pf.refcfg[key])) except KeyError: pass print("!! cfg[%s] = %s" % (key, pf.cfg[key]))
[docs]def savePreferences(): """Save the preferences. The name of the preferences file is determined at startup from the configuration files, and saved in ``pyformex.preffile``. If a local preferences file was read, it will be saved there. Otherwise, it will be saved as the user preferences, possibly creating that file. If ``pyformex.preffile`` is None, preferences are not saved. """ pf.debug("savePreferences to: %s" % pf.preffile, pf.DEBUG.CONFIG) if pf.preffile is None: return # Create the user conf dir # try: pf.preffile.parent.mkdir(parents=True, exist_ok=True) # except Exception: # print("The path where your user preferences should be stored can" # "not be created!\nPreferences are not saved!") # return # print(""" # ############################ # ## Currently I can not save the preferences in the new format! # ## After all, this is a development version ;) # ############################################################### # """) # return # Do not store the refcfg warning filters: we add them on startup a = set(pf.prefcfg['warnings/filters']) b = set(pf.refcfg['warnings/filters']) pf.prefcfg['warnings/filters'] = a-b # Currently erroroneously processed, therefore not saved del pf.prefcfg['render/light0'] del pf.prefcfg['render/light1'] del pf.prefcfg['render/light2'] del pf.prefcfg['render/light3'] pf.debug("=" * 60, pf.DEBUG.CONFIG) pf.debug("!!!Saving config:\n%s" % pf.prefcfg, pf.DEBUG.CONFIG) try: pf.debug("Saving preferences to file %s" % pf.preffile, pf.DEBUG.CONFIG) pf.prefcfg.write(pf.preffile) res = "Saved" except Exception: res = "Could not save" raise pf.debug("%s preferences to file %s" % (res, pf.preffile), pf.DEBUG.CONFIG) return res == "Saved"
[docs]def activateWarningFilters(): """Activate the warning filters First time activation of the warning filters and customized warning formatting. """ from pyformex import messages utils.resetWarningFilters() def _format_warning(message, category, filename, lineno, line=None): """Replace the default warnings.formatwarning This allows the warnings being called using a simple mnemonic string. The full message is then found from the message module. """ message = messages.getMessage(message) message = """.. pyFormex Warning ================ %s `%s called from:` %s `line:` %s """ % (message, category.__name__, filename, lineno) if line: message += "%s\n" % line return message if pf.cfg['warnings/nice']: warnings.formatwarning = _format_warning
# End