#
##
## 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/.
##
"""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 os
import warnings
import pyformex as pf
from pyformex.path import Path
from pyformex import software
# Note: This module and all of the above should NOT import numpy
[docs]def whereami():
"""Report where pyFormex is installed"""
import sys
from inspect import cleandoc as dedent
return dedent(
f"""{pf.fullversion()}
pyFormex executable: {pf._executable}
pyFormex installation ({pf._installtype}): {pf.pyformexdir}
Python sys.path: {sys.path}
""")
[docs]def run_docmodule(module):
"""Print autogenerated documentation for the module.
module is a pyFormex module dotted path. The leading pyformex.
may be omitted.
"""
from pyformex import py2rst
out = py2rst.do_module(module)
refdir = pf.cfg['sphinxdir'] / 'ref'
if refdir.exists():
outfile = refdir / module + '.rst'
outfile.write_text(out)
print(f"Wrote {outfile}")
else:
raise RuntimeError(
f"The ref directory {refdir} does not exist")
[docs]def moduleList(package='all'):
"""Return a list of all pyFormex modules in a subpackage.
This is like :func:`~pyformex.utils.sourceFiles`, but returns the files in a
Python module syntax.
"""
import re
from pyformex import utils
exclude = ['examples', 'scripts', 'opengl3']
files = utils.sourceFiles(relative=True)
directory = os.path.split(files[0])[0]
dirlen = len(directory)
if dirlen > 0:
dirlen += 1
# Retain only .py files and convert to Python pkg.module format
modules = [fn[dirlen:].replace('.py', '').replace('/', '.')
for fn in files if fn.endswith('.py')]
if package == 'core':
# only core
modules = [m for m in modules if '.' not in m]
elif package == 'all':
# everything except examples
modules = [m for m in modules if m.split('.')[0] not in exclude]
elif package == 'all+ex':
# everything including examples
pass
else:
if package.endswith('+'):
# Also include subpackages
package = package[:-1]
modules = [m for m in modules if m.startswith(package+'.')]
else:
modules = [m for m in modules
if re.fullmatch(f"{package}[.][^.]+", m)]
modules = sorted(modules, key=str.casefold)
for i, m in enumerate(modules):
if m.endswith('__init__'):
if m == '__init__':
modules[i] = 'pyformex'
else:
modules[i] = m[:-9]
return modules
# TODO: This is hardcoded here because we call
# pyformex --listmodules subpkg --sphinx
# from two places. We should probably set it in some config file, so that
# cmdtools does not need to be changed all the time.
_sphinx_fail_modules = {
'core': ['core', '_exec', '_startup'],
'gui': ['gui', 'gui.guicore', 'gui.draw'],
'gui.menus': ['gui.menus.Draw2d' ],
'fe': ['fe', 'fe.fe_ast'],
'plugins': [],
'opengl': ['opengl'],
'lib': [],
'apps': ['apps.bifmesh', 'apps.FeEx', 'apps.Hesperia'],
}
[docs]def list_modules(pkgs=['all'], sphinx=False):
"""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'].
sphinx: bool
If True, some modules that are known to fail with sphinx are
not listed. This is mainly intended for use from within sphinx.
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:
mods = moduleList(subpkg)
if sphinx:
mods = [m for m in mods
if m not in _sphinx_fail_modules.get(subpkg, [])]
modules.extend(mods)
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
# warnings.filterwarnings("always", category=DeprecationWarning)
# warnings.filterwarnings("always", category=PendingDeprecationWarning)
testpath = pf.cfg['testdir']
# print(f"TESTPATH={testpath}")
args = ['--maxfail=10', '-W', 'always::DeprecationWarning']
if not modules:
pytest.main(args + [testpath])
else:
print(f"Running pytests for modules {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(f"No such test module: {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.
"""
## Initialize numpy ##
# Note: this will use factory settings
pf._startup.initialize_numpy(version=pf._startup.numpy_version,
printoptions=pf.cfg['numpy/printoptions'],
typelessdata=pf.cfg['numpy/typelessdata'])
pf._doctest = True
# pf.error = print
# safe_verbosityset verbosity to 0
# pf.options.verbose = 0
# This has to be run from parentdir
save_dir = Path.cwd()
run_dir = pf.pyformexdir.parent
# we need a writable workdir
if save_dir.is_writable_dir():
work_dir = save_dir
else:
work_dir = Path.home() / '.local' / 'pyformex' / 'workdir'
Path.mkdir(work_dir, parents=True, exist_ok=True)
os.chdir(run_dir)
if not modules:
modules = ['all']
todo = []
for mod in modules:
if mod in ['all'] or mod.endswith('.'):
todo.extend(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)
# Remove modules that we can not test:
todo = [m for m in todo if 'vtk' not in m]
# Remove plugins.mesh_io if we do not have meshio
if not software.Module.has('meshio'):
m = 'plugins.mesh_io'
if m in todo:
todo.remove(m)
if pf.debugon(pf.DEBUG.DOCTEST):
print("Final list of modules to test:", todo)
# Now perform the tests
FAILED, failed, attempted = 0, 0, 0
os.chdir(work_dir)
for m in todo:
try:
result = doctest_module(m)
failed += result.failed
attempted += result.attempted
except Exception as e:
if pf.verbosity(2):
raise e
result = f"FAIL\n Failed because: {e}"
FAILED += 1
print(f"Module {m}: {result}")
os.chdir(save_dir)
if len(todo) > 1:
print('-'*60)
print(f"Totals: attempted={attempted} tests, failed={failed} tests, "
f"FAILED={FAILED}/{len(todo)} 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
import numpy as np
if not module.startswith('pyformex'):
module = 'pyformex.' + module
mod = importlib.import_module(module)
printoptions = getattr(mod, '_numpy_printoptions_', {})
pf.options.verbose = 0
with warnings.catch_warnings():
warnings.simplefilter("ignore")
# Always activate when running doctests
warnings.filterwarnings("default", category=DeprecationWarning)
warnings.filterwarnings("default", category=PendingDeprecationWarning)
if pf.debugon(pf.DEBUG.DOCTEST):
print("Running doctests on", mod)
with np.printoptions(**printoptions):
from pyformex.project import Project
pf.PF = Project()
return doctest.testmod(
mod, optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS)
[docs]def processOptions(options):
"""Process options that run one of the command tools"""
if options.whereami:
print(whereami())
return 0
if options.listmodules is not None:
print('\n'.join(list_modules(options.listmodules, options.sphinx)))
return 0
# The following options require the core
pf._import_core()
if options.detect:
from pyformex import software
# from pyformex import _main
# _main.override_config('bindings', 'gui/bindings')
# print("DETECT", pf.options)
# from pyformex import gui # noqa: F401 (to get correct bindings)
pf.software.Module.unload = True # allows detection of all bindings
print("Detecting installed helper software")
print(software.reportSoftware(color=True))
return 0
if options.doctest is not None:
run_doctest(options.doctest)
return 0
if options.pytest is not None:
run_pytest(options.pytest)
return 0
if options.docmodule is not None:
pf._sphinx = True
for a in options.docmodule:
run_docmodule(a)
return 0
if options.remove:
print(whereami())
remove_pyFormex(pf.pyformexdir, pf._executable)
# After this option, we can not continue,
# so this should be the last option processed
return 0
# End