From 23f50092bf4af21742e834ef7b908102c85af4d8 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Tue, 9 Nov 2021 18:23:35 -0800 Subject: [PATCH 01/29] sage.features.normaliz: New --- src/sage/features/normaliz.py | 28 +++++++++++++++++++ .../geometry/polyhedron/backend_normaliz.py | 4 +-- src/sage/geometry/polyhedron/base.py | 6 ++-- 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 src/sage/features/normaliz.py diff --git a/src/sage/features/normaliz.py b/src/sage/features/normaliz.py new file mode 100644 index 00000000000..5135b7254ab --- /dev/null +++ b/src/sage/features/normaliz.py @@ -0,0 +1,28 @@ +r""" +Check for pynormaliz +""" +from . import PythonModule +from .join_feature import JoinFeature + + +class pynormaliz(JoinFeature): + r""" + A :class:`sage.features.Feature` describing the presence of the + Python package ``igraph``. + + EXAMPLES:: + + sage: from sage.features.igraph import pynormaliz + sage: pynormaliz().is_present() # optional - pynormaliz + FeatureTestResult('pynormaliz', True) + """ + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.igraph import pynormaliz + sage: isinstance(pynormaliz(), pynormaliz) + True + """ + JoinFeature.__init__(self, 'pynormaliz', + [PythonModule('PyNormaliz', spkg="pynormaliz")]) diff --git a/src/sage/geometry/polyhedron/backend_normaliz.py b/src/sage/geometry/polyhedron/backend_normaliz.py index d1f30e2d31f..7c48b54cd96 100644 --- a/src/sage/geometry/polyhedron/backend_normaliz.py +++ b/src/sage/geometry/polyhedron/backend_normaliz.py @@ -26,10 +26,10 @@ from sage.structure.element import Element from sage.misc.cachefunc import cached_method from sage.misc.misc_c import prod -from sage.features import PythonModule +from sage.features.normaliz import pynormaliz from sage.misc.lazy_import import lazy_import lazy_import('PyNormaliz', ['NmzResult', 'NmzCompute', 'NmzCone', 'NmzConeCopy'], - feature=PythonModule("PyNormaliz", spkg="pynormaliz")) + feature=pynormaliz) from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ diff --git a/src/sage/geometry/polyhedron/base.py b/src/sage/geometry/polyhedron/base.py index d44c3d508fb..f6969eab1eb 100644 --- a/src/sage/geometry/polyhedron/base.py +++ b/src/sage/geometry/polyhedron/base.py @@ -8729,16 +8729,18 @@ def volume(self, measure='ambient', engine='auto', **kwds): Latte().require() engine = 'latte' except FeatureNotPresentError: + from sage.features.normaliz import pynormaliz try: - PythonModule("PyNormaliz", spkg="pynormaliz").require() + pynormaliz.require() engine = 'normaliz' except FeatureNotPresentError: raise RuntimeError("the induced rational measure can only be computed with the optional packages `latte_int`, or `pynormaliz`") if engine == 'auto' and measure == 'induced_lattice': # Enforce a default choice, change if a better engine is found. + from sage.features.normaliz import pynormaliz try: - PythonModule("PyNormaliz", spkg="pynormaliz").require() + pynormaliz.require() engine = 'normaliz' except FeatureNotPresentError: try: From 0b65b799ed5bc66fed7e1e39c1a07980a3a08616 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Tue, 9 Nov 2021 19:58:58 -0800 Subject: [PATCH 02/29] src/sage/features/normaliz.py: Fix up --- src/sage/features/normaliz.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sage/features/normaliz.py b/src/sage/features/normaliz.py index 5135b7254ab..e00077011aa 100644 --- a/src/sage/features/normaliz.py +++ b/src/sage/features/normaliz.py @@ -8,11 +8,11 @@ class pynormaliz(JoinFeature): r""" A :class:`sage.features.Feature` describing the presence of the - Python package ``igraph``. + Python package ``PyNormaliz``. EXAMPLES:: - sage: from sage.features.igraph import pynormaliz + sage: from sage.features.normaliz import pynormaliz sage: pynormaliz().is_present() # optional - pynormaliz FeatureTestResult('pynormaliz', True) """ @@ -20,7 +20,7 @@ def __init__(self): r""" TESTS:: - sage: from sage.features.igraph import pynormaliz + sage: from sage.features.normaliz import pynormaliz sage: isinstance(pynormaliz(), pynormaliz) True """ From 00cb9c7fe2e9dacb1270d556e51b2a8e08bf92f4 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 10 Nov 2021 16:13:54 -0800 Subject: [PATCH 03/29] src/sage/features/normaliz.py: Rename class to PyNormaliz, fix uses --- src/sage/features/normaliz.py | 10 +++++----- src/sage/geometry/polyhedron/backend_normaliz.py | 4 ++-- src/sage/geometry/polyhedron/base.py | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sage/features/normaliz.py b/src/sage/features/normaliz.py index e00077011aa..ceb67875b01 100644 --- a/src/sage/features/normaliz.py +++ b/src/sage/features/normaliz.py @@ -5,23 +5,23 @@ from .join_feature import JoinFeature -class pynormaliz(JoinFeature): +class PyNormaliz(JoinFeature): r""" A :class:`sage.features.Feature` describing the presence of the Python package ``PyNormaliz``. EXAMPLES:: - sage: from sage.features.normaliz import pynormaliz - sage: pynormaliz().is_present() # optional - pynormaliz + sage: from sage.features.normaliz import PyNormaliz + sage: PyNormaliz().is_present() # optional - pynormaliz FeatureTestResult('pynormaliz', True) """ def __init__(self): r""" TESTS:: - sage: from sage.features.normaliz import pynormaliz - sage: isinstance(pynormaliz(), pynormaliz) + sage: from sage.features.normaliz import PyNormaliz + sage: isinstance(PyNormaliz(), PyNormaliz) True """ JoinFeature.__init__(self, 'pynormaliz', diff --git a/src/sage/geometry/polyhedron/backend_normaliz.py b/src/sage/geometry/polyhedron/backend_normaliz.py index 7c48b54cd96..4b413193c26 100644 --- a/src/sage/geometry/polyhedron/backend_normaliz.py +++ b/src/sage/geometry/polyhedron/backend_normaliz.py @@ -26,10 +26,10 @@ from sage.structure.element import Element from sage.misc.cachefunc import cached_method from sage.misc.misc_c import prod -from sage.features.normaliz import pynormaliz from sage.misc.lazy_import import lazy_import +import sage.features.normaliz lazy_import('PyNormaliz', ['NmzResult', 'NmzCompute', 'NmzCone', 'NmzConeCopy'], - feature=pynormaliz) + feature=sage.features.normaliz.PyNormaliz()) from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ diff --git a/src/sage/geometry/polyhedron/base.py b/src/sage/geometry/polyhedron/base.py index f6969eab1eb..354672ba01d 100644 --- a/src/sage/geometry/polyhedron/base.py +++ b/src/sage/geometry/polyhedron/base.py @@ -8729,18 +8729,18 @@ def volume(self, measure='ambient', engine='auto', **kwds): Latte().require() engine = 'latte' except FeatureNotPresentError: - from sage.features.normaliz import pynormaliz + from sage.features.normaliz import PyNormaliz try: - pynormaliz.require() + PyNormaliz().require() engine = 'normaliz' except FeatureNotPresentError: raise RuntimeError("the induced rational measure can only be computed with the optional packages `latte_int`, or `pynormaliz`") if engine == 'auto' and measure == 'induced_lattice': # Enforce a default choice, change if a better engine is found. - from sage.features.normaliz import pynormaliz + from sage.features.normaliz import PyNormaliz try: - pynormaliz.require() + PyNormaliz().require() engine = 'normaliz' except FeatureNotPresentError: try: From 7632e539cf7c7bfa15e8be3ed2cb7d6804d4ce27 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 10 Nov 2021 17:04:07 -0800 Subject: [PATCH 04/29] src/sage/geometry/polyhedron/base.py: Remove unused import --- src/sage/geometry/polyhedron/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/geometry/polyhedron/base.py b/src/sage/geometry/polyhedron/base.py index 354672ba01d..32a95a8b7a6 100644 --- a/src/sage/geometry/polyhedron/base.py +++ b/src/sage/geometry/polyhedron/base.py @@ -8717,7 +8717,7 @@ def volume(self, measure='ambient', engine='auto', **kwds): sage: Q.volume.is_in_cache() True """ - from sage.features import FeatureNotPresentError, PythonModule + from sage.features import FeatureNotPresentError if measure == 'induced_rational' and engine not in ['auto', 'latte', 'normaliz']: raise RuntimeError("the induced rational measure can only be computed with the engine set to `auto`, `latte`, or `normaliz`") if measure == 'induced_lattice' and engine not in ['auto', 'latte', 'normaliz']: From fa45c36e6009c1bafaed18efb75eaac975ab6aa3 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 10 Nov 2021 17:58:39 -0800 Subject: [PATCH 05/29] sage.features.sagemath.all_features: New --- src/sage/features/sagemath.py | 40 ++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/sage/features/sagemath.py b/src/sage/features/sagemath.py index 2d7d6e9ab79..4c5ae934015 100644 --- a/src/sage/features/sagemath.py +++ b/src/sage/features/sagemath.py @@ -162,7 +162,7 @@ def __init__(self): spkg="sagemath_symbolics") -def sage_features(logger=None): +def all_features(): """ Return features corresponding to parts of the Sage library. @@ -177,6 +177,36 @@ def sage_features(logger=None): Instead, we associate distribution packages to Python modules in :mod:`sage.features.sagemath` via the ``spkg`` parameter of :class:`Feature`. + EXAMPLES:: + + sage: from sage.features.sagemath import all_features + sage: list(all_features()) + [Feature('sage.combinat'), ...] + """ + return [sage__combinat(), + sage__geometry__polyhedron(), + sage__graphs(), + sage__plot(), + sage__rings__number_field(), + sage__rings__real_double(), + sage__symbolic()] + + +def sage_features(logger=None): + """ + Return features corresponding to parts of the Sage library that are present. + + These tags are named after Python packages/modules (e.g., :mod:`~sage.symbolic`), + not distribution packages (``sagemath-symbolics``). + + This design is motivated by a separation of concerns: The author of a module that depends + on some functionality provided by a Python module usually already knows the + name of the Python module, so we do not want to force the author to also + know about the distribution package that provides the Python module. + + Instead, we associate distribution packages to Python modules in + :mod:`sage.features.sagemath` via the ``spkg`` parameter of :class:`Feature`. + EXAMPLES:: sage: from sage.features.sagemath import sage_features @@ -186,13 +216,7 @@ def sage_features(logger=None): Feature('sage.rings.number_field'), Feature('sage.rings.real_double')] """ - for feature in [sage__combinat(), - sage__geometry__polyhedron(), - sage__graphs(), - sage__plot(), - sage__rings__number_field(), - sage__rings__real_double(), - sage__symbolic()]: + for feature in all_features(): result = feature.is_present() if logger: logger.write(f'{result}, reason: {result.reason}\n') From 17e30e2660400b7825e0a14c10b09da5458bb59d Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 10 Nov 2021 17:59:04 -0800 Subject: [PATCH 06/29] sage.features.all: New --- src/sage/features/all.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/sage/features/all.py diff --git a/src/sage/features/all.py b/src/sage/features/all.py new file mode 100644 index 00000000000..dc13b375959 --- /dev/null +++ b/src/sage/features/all.py @@ -0,0 +1,24 @@ + +def all_features(): + r""" + Return an iterable of all features. + + EXAMPLES:: + + sage: from sage.features.all import all_features + sage: sorted(all_features(), key=lambda f: f.name) # random + [...Feature('sage.combinat')...] + """ + import pkgutil + import importlib + import sage.features + # Following https://packaging.python.org/guides/creating-and-discovering-plugins/#using-namespace-packages + for finder, name, ispkg in pkgutil.iter_modules(sage.features.__path__, sage.features.__name__ + "."): + module = importlib.import_module(name) + try: + af = module.all_features + except AttributeError: + pass + else: + if af != all_features: + yield from af() From 8bfebc9c04fc997d2595babb9cc2f78df4184b2c Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 10 Nov 2021 18:06:03 -0800 Subject: [PATCH 07/29] sage.features.bliss.all_features: New --- src/sage/features/bliss.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sage/features/bliss.py b/src/sage/features/bliss.py index d870ade5973..6171f9935ce 100644 --- a/src/sage/features/bliss.py +++ b/src/sage/features/bliss.py @@ -69,3 +69,8 @@ def __init__(self): JoinFeature.__init__(self, "bliss", [PythonModule("sage.graphs.bliss", spkg="bliss", url="http://www.tcs.hut.fi/Software/bliss/")]) + + +def all_features(): + return [BlissLibrary(), + Bliss()] From 893ec23c44ae4c85c4297f2b06ccb102673e8582 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 10 Nov 2021 19:09:34 -0800 Subject: [PATCH 08/29] src/sage/features/kenzo.py: Move import from sage.libs.ecl into method --- src/sage/features/kenzo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/features/kenzo.py b/src/sage/features/kenzo.py index 7012dc230ec..f1778d9a379 100644 --- a/src/sage/features/kenzo.py +++ b/src/sage/features/kenzo.py @@ -3,7 +3,6 @@ Check for Kenzo """ -from sage.libs.ecl import ecl_eval from . import Feature, FeatureTestResult class Kenzo(Feature): @@ -37,6 +36,7 @@ def _is_present(self): sage: Kenzo()._is_present() # optional - kenzo FeatureTestResult('kenzo', True) """ + from sage.libs.ecl import ecl_eval # Redirection of ECL and Maxima stdout to /dev/null # This is also done in the Maxima library, but we # also do it here for redundancy. From eaec4000ec31e884e8e152efbd88e1b15ff3c5a3 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sat, 13 Nov 2021 11:48:50 -0800 Subject: [PATCH 09/29] src/sage/features/interfaces.py: New --- src/sage/features/interfaces.py | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/sage/features/interfaces.py diff --git a/src/sage/features/interfaces.py b/src/sage/features/interfaces.py new file mode 100644 index 00000000000..a3e86c75e8d --- /dev/null +++ b/src/sage/features/interfaces.py @@ -0,0 +1,92 @@ +r""" +Check for working interpreter interfaces +""" +import importlib + +from . import Feature, FeatureTestResult, PythonModule + + +class InterfaceFeature(Feature): + + @staticmethod + def __classcall__(cls, name, module, description=None): + if isinstance(module, str): + module = PythonModule(module) + return Feature.__classcall__(cls, name, module, description) + + def __init__(self, name, module, description): + super().__init__(name, description=description) + self.module = module + + def _is_present(self): + result = self.module.is_present() + if not result: + return result + m = importlib.import_module(self.module.name) + try: + interface = getattr(m, self.name) + interface('2+3') + return FeatureTestResult(self, True) + except Exception as exception: + return FeatureTestResult(self, False, + reason=f"Interface {interface} is not functional: {exception}") + +# The following are provided by external software only (no SPKG) + +class Magma(InterfaceFeature): + r""" + A :class:`sage.features.Feature` describing whether :class:`sage.interfaces.magma.Magma` + is present and functional. + + EXAMPLES:: + + sage: from sage.features.interfaces import Magma + sage: Magma().is_present() # random + FeatureTestResult('jupymake', False) + """ + + @staticmethod + def __classcall__(cls): + return InterfaceFeature(cls, 'magma', 'sage.interfaces.magma') + + +class Matlab(InterfaceFeature): + + @staticmethod + def __classcall__(cls): + return InterfaceFeature(cls, 'matlab', 'sage.interfaces.matlab') + + +class Mathematica(InterfaceFeature): + + @staticmethod + def __classcall__(cls): + return InterfaceFeature(cls, 'mathematica', 'sage.interfaces.mathematica') + + +class Maple(InterfaceFeature): + + @staticmethod + def __classcall__(cls): + return InterfaceFeature(cls, 'maple', 'sage.interfaces.maple') + + +class Macaulay2(InterfaceFeature): + + @staticmethod + def __classcall__(cls): + return InterfaceFeature(cls, 'macaulay2', 'sage.interfaces.macaulay2') + + +class Octave(InterfaceFeature): + + @staticmethod + def __classcall__(cls): + return InterfaceFeature(cls, 'octave', 'sage.interfaces.octave') + + +class Scilab(InterfaceFeature): + + @staticmethod + def __classcall__(cls): + return InterfaceFeature(cls, 'scilab', 'sage.interfaces.scilab') From 827ca0339457bc5596fce9cb46aefc7fe8d6e002 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 13:00:22 -0800 Subject: [PATCH 10/29] src/sage/features/interfaces.py: Fix and doctest exception handling --- src/sage/features/interfaces.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/sage/features/interfaces.py b/src/sage/features/interfaces.py index a3e86c75e8d..8df56ab0d3d 100644 --- a/src/sage/features/interfaces.py +++ b/src/sage/features/interfaces.py @@ -7,7 +7,26 @@ class InterfaceFeature(Feature): - + r""" + TESTS:: + + sage: from sage.features.interfaces import InterfaceFeature + sage: broken = InterfaceFeature("broken_interface", "sage.interfaces.does_not_exist") + sage: broken.is_present() + FeatureTestResult('sage.interfaces.does_not_exist', False) + sage: _.reason + "Failed to import `sage.interfaces.does_not_exist`: No module named 'sage.interfaces.does_not_exist'" + + sage: also_broken = InterfaceFeature("also_broken_interface", "sage.interfaces.interface") + sage: also_broken.is_present() + Traceback (most recent call last): + ... + FeatureTestResult('also_broken_interface', False) + sage: _.reason + Traceback (most recent call last): + ... + "Interface also_broken_interface cannot be imported: module 'sage.interfaces.interface' has no attribute 'also_broken_interface'" + """ @staticmethod def __classcall__(cls, name, module, description=None): if isinstance(module, str): @@ -25,6 +44,10 @@ def _is_present(self): m = importlib.import_module(self.module.name) try: interface = getattr(m, self.name) + except Exception as exception: + return FeatureTestResult(self, False, + reason=f"Interface {self.name} cannot be imported: {exception}") + try: interface('2+3') return FeatureTestResult(self, True) except Exception as exception: From 52ac51efe425371ab7aab08d3eb964bf4c90cd4c Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 13:00:46 -0800 Subject: [PATCH 11/29] src/sage/features/interfaces.py (all_features): New --- src/sage/features/interfaces.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/sage/features/interfaces.py b/src/sage/features/interfaces.py index 8df56ab0d3d..2f7a9e7d61c 100644 --- a/src/sage/features/interfaces.py +++ b/src/sage/features/interfaces.py @@ -113,3 +113,23 @@ class Scilab(InterfaceFeature): @staticmethod def __classcall__(cls): return InterfaceFeature(cls, 'scilab', 'sage.interfaces.scilab') + + +def all_features(): + r""" + Return features corresponding to interpreter interfaces. + + EXAMPLES:: + + sage: from sage.features.interfaces import all_features + sage: list(all_features()) + [Feature(: sage.interfaces.magma), ..., + Feature(: sage.interfaces.scilab)] + """ + return [Magma(), + Matlab(), + Mathematica(), + Maple(), + Macaulay2(), + Octave(), + Scilab()] From 7754fae5f46ef3ad9fc2c9c3bfd2a2dac3f76f11 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 13:25:16 -0800 Subject: [PATCH 12/29] src/sage/features/interfaces.py: Fix and doctest exception handling (fixup) --- src/sage/features/interfaces.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sage/features/interfaces.py b/src/sage/features/interfaces.py index 2f7a9e7d61c..21b8b01445f 100644 --- a/src/sage/features/interfaces.py +++ b/src/sage/features/interfaces.py @@ -19,12 +19,8 @@ class InterfaceFeature(Feature): sage: also_broken = InterfaceFeature("also_broken_interface", "sage.interfaces.interface") sage: also_broken.is_present() - Traceback (most recent call last): - ... FeatureTestResult('also_broken_interface', False) sage: _.reason - Traceback (most recent call last): - ... "Interface also_broken_interface cannot be imported: module 'sage.interfaces.interface' has no attribute 'also_broken_interface'" """ @staticmethod From 8081f29f97a947e39b8ef17e861433847e623dcd Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 13:25:31 -0800 Subject: [PATCH 13/29] src/sage/doctest/external.py: Use sage.features.interfaces --- src/sage/doctest/external.py | 48 +++++++++--------------------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index e74b3554e50..cdd09eb464b 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -117,12 +117,8 @@ def has_magma(): sage: has_magma() # random, optional - magma True """ - from sage.interfaces.magma import magma - try: - magma('2+3') - return True - except Exception: - return False + from sage.features.interfaces import Magma + return Magma().is_present() def has_matlab(): """ @@ -134,12 +130,8 @@ def has_matlab(): sage: has_matlab() # random, optional - matlab True """ - from sage.interfaces.matlab import matlab - try: - matlab('2+3') - return True - except Exception: - return False + from sage.features.interfaces import Matlab + return Matlab().is_present() def has_mathematica(): """ @@ -151,12 +143,8 @@ def has_mathematica(): sage: has_mathematica() # random, optional - mathematica True """ - from sage.interfaces.mathematica import mathematica - try: - mathematica('2+3') - return True - except Exception: - return False + from sage.features.interfaces import Mathematica + return Mathematica().is_present() def has_maple(): """ @@ -168,12 +156,8 @@ def has_maple(): sage: has_maple() # random, optional - maple True """ - from sage.interfaces.maple import maple - try: - maple('2+3') - return True - except Exception: - return False + from sage.features.interfaces import Maple + return Maple().is_present() def has_macaulay2(): """ @@ -185,12 +169,8 @@ def has_macaulay2(): sage: has_macaulay2() # random, optional - macaulay2 True """ - from sage.interfaces.macaulay2 import macaulay2 - try: - macaulay2('2+3') - return True - except Exception: - return False + from sage.features.interfaces import Macaulay2 + return Macaulay2().is_present() def has_octave(): """ @@ -202,12 +182,8 @@ def has_octave(): sage: has_octave() # random, optional - octave True """ - from sage.interfaces.octave import octave - try: - octave('2+3') - return True - except Exception: - return False + from sage.features.interfaces import Octave + return Octave().is_present() def has_pandoc(): """ From c41808ba23b81ac2c930fb77ea9ded20a7b7d770 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 14:03:39 -0800 Subject: [PATCH 14/29] src/sage/features/latex.py (all_features): New --- src/sage/features/latex.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sage/features/latex.py b/src/sage/features/latex.py index a0f0d6f4096..0890320b39b 100644 --- a/src/sage/features/latex.py +++ b/src/sage/features/latex.py @@ -136,3 +136,10 @@ def __init__(self): """ Executable.__init__(self, "lualatex", executable="lualatex", url="https://www.latex-project.org/") + + +def all_features(): + return [latex(), + pdflatex(), + xelatex(), + lualatex()] From 44c19e3145fb47c7d977a326020ecf2be3298776 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 14:03:53 -0800 Subject: [PATCH 15/29] src/sage/features/mip_backends.py (all_features): New --- src/sage/features/mip_backends.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/sage/features/mip_backends.py b/src/sage/features/mip_backends.py index bec5d958247..6bf3f936f26 100644 --- a/src/sage/features/mip_backends.py +++ b/src/sage/features/mip_backends.py @@ -71,3 +71,9 @@ def __init__(self): JoinFeature.__init__(self, 'sage_numerical_backends_coin', [MIPBackend('coin')], spkg='sage_numerical_backends_coin') + + +def all_features(): + return [CPLEX(), + Gurobi(), + COIN()] From fa25155f55cadf4424dd561586b111e4183e2d50 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 14:04:39 -0800 Subject: [PATCH 16/29] src/sage/doctest/external.py (external_software): Go through external_features --- src/sage/doctest/external.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index cdd09eb464b..7e73dc60577 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -333,6 +333,32 @@ def has_4ti2(): from sage.features.four_ti_2 import FourTi2 return FourTi2().is_present() +def external_features(): + features = [] + from sage.features.internet import Internet + features.append(Internet()) + import sage.features.latex + features.extend(sage.features.latex.all_features()) + import sage.features.interfaces + features.extend(sage.features.interfaces.all_features()) + from sage.features.pandoc import Pandoc + features.append(Pandoc()) + from sage.features.graphviz import Graphviz + features.append(Graphviz()) + from sage.features.ffmpeg import FFmpeg + features.append(FFmpeg()) + from sage.features.imagemagick import ImageMagick + features.append(ImageMagick()) + from sage.features.dvipng import dvipng + features.append(dvipng()) + from sage.features.pdf2svg import pdf2svg + features.append(pdf2svg()) + from sage.features.rubiks import Rubiks + features.append(Rubiks()) + from sage.features.four_ti_2 import FourTi2 + features.append(FourTi2()) + return features + def external_software(): """ Return the alphabetical list of external software supported by this module. @@ -343,11 +369,7 @@ def external_software(): sage: sorted(external_software) == external_software True """ - supported = list() - for func in globals(): - if func.startswith(prefix): - supported.append(func[len(prefix):]) - return sorted(supported) + return sorted(f.name for f in external_features()) external_software = external_software() From 945132b9524a6734957e70a00830b6db07f74b65 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 14:16:05 -0800 Subject: [PATCH 17/29] src/sage/features/interfaces.py: Fix up --- src/sage/features/interfaces.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/sage/features/interfaces.py b/src/sage/features/interfaces.py index 21b8b01445f..231fba136b2 100644 --- a/src/sage/features/interfaces.py +++ b/src/sage/features/interfaces.py @@ -66,49 +66,49 @@ class Magma(InterfaceFeature): @staticmethod def __classcall__(cls): - return InterfaceFeature(cls, 'magma', 'sage.interfaces.magma') + return InterfaceFeature.__classcall__(cls, 'magma', 'sage.interfaces.magma') class Matlab(InterfaceFeature): @staticmethod def __classcall__(cls): - return InterfaceFeature(cls, 'matlab', 'sage.interfaces.matlab') + return InterfaceFeature.__classcall__(cls, 'matlab', 'sage.interfaces.matlab') class Mathematica(InterfaceFeature): @staticmethod def __classcall__(cls): - return InterfaceFeature(cls, 'mathematica', 'sage.interfaces.mathematica') + return InterfaceFeature.__classcall__(cls, 'mathematica', 'sage.interfaces.mathematica') class Maple(InterfaceFeature): @staticmethod def __classcall__(cls): - return InterfaceFeature(cls, 'maple', 'sage.interfaces.maple') + return InterfaceFeature.__classcall__(cls, 'maple', 'sage.interfaces.maple') class Macaulay2(InterfaceFeature): @staticmethod def __classcall__(cls): - return InterfaceFeature(cls, 'macaulay2', 'sage.interfaces.macaulay2') + return InterfaceFeature.__classcall__(cls, 'macaulay2', 'sage.interfaces.macaulay2') class Octave(InterfaceFeature): @staticmethod def __classcall__(cls): - return InterfaceFeature(cls, 'octave', 'sage.interfaces.octave') + return InterfaceFeature.__classcall__(cls, 'octave', 'sage.interfaces.octave') class Scilab(InterfaceFeature): @staticmethod def __classcall__(cls): - return InterfaceFeature(cls, 'scilab', 'sage.interfaces.scilab') + return InterfaceFeature.__classcall__(cls, 'scilab', 'sage.interfaces.scilab') def all_features(): @@ -119,8 +119,13 @@ def all_features(): sage: from sage.features.interfaces import all_features sage: list(all_features()) - [Feature(: sage.interfaces.magma), ..., - Feature(: sage.interfaces.scilab)] + [Feature('magma'), + Feature('matlab'), + Feature('mathematica'), + Feature('maple'), + Feature('macaulay2'), + Feature('octave'), + Feature('scilab')] """ return [Magma(), Matlab(), From 7fa5b3bf93c320f0d0d388b7c7c240dfe51d8300 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 14:24:55 -0800 Subject: [PATCH 18/29] src/sage/doctest/external.py (external_features): Add CPLEX, Gurobi --- src/sage/doctest/external.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index 7e73dc60577..2736ed09086 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -341,6 +341,8 @@ def external_features(): features.extend(sage.features.latex.all_features()) import sage.features.interfaces features.extend(sage.features.interfaces.all_features()) + from sage.features.mip_backends import CPLEX, Gurobi + features.extend([CPLEX(), Gurobi()]) from sage.features.pandoc import Pandoc features.append(Pandoc()) from sage.features.graphviz import Graphviz From 132f00dfbe947bb3898debb5ccf659b728624fb9 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 15:43:31 -0800 Subject: [PATCH 19/29] sage.doctest.{control,external}: Detect optional tags via AvailableSoftware --- src/sage/doctest/control.py | 15 ++++----------- src/sage/doctest/external.py | 35 ++++++++++++++++------------------- src/sage/doctest/parsing.py | 3 +-- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py index 251e4f34a43..37f7f469d37 100644 --- a/src/sage/doctest/control.py +++ b/src/sage/doctest/control.py @@ -409,11 +409,6 @@ def __init__(self, options, args): options.optional.update(system.name for system in package_systems()) - logger = sys.stderr if options.verbose else None - from sage.features.sagemath import sage_features - options.optional.update(feature.name - for feature in sage_features(logger=logger)) - # Check that all tags are valid for o in options.optional: if not optionaltag_regex.search(o): @@ -1247,18 +1242,16 @@ def run(self): pass self.log("Using --optional=" + self._optional_tags_string()) - if self.options.optional is True or 'external' in self.options.optional: - self.log("External software to be detected: " + ','.join(external_software)) - + self.log("External software to be detected: " + ','.join(external_software)) + available_software._allow_external = self.options.optional is True or 'external' in self.options.optional self.add_files() self.expand_files_into_sources() self.filter_sources() self.sort_sources() self.run_doctests() - if self.options.optional is True or 'external' in self.options.optional: - self.log("External software detected for doctesting: " - + ','.join(available_software.seen())) + self.log("Features detected for doctesting: " + + ','.join(available_software.seen())) self.cleanup() return self.reporter.error_status diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index 2736ed09086..882227fdbe1 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -375,20 +375,6 @@ def external_software(): external_software = external_software() -def _lookup(software): - """ - Test if the software is available on the system. - - EXAMPLES:: - - sage: sage.doctest.external._lookup('internet') # random, optional - internet - True - """ - if software in external_software: - func = globals().get(prefix + software) - return func() - else: - return False class AvailableSoftware(object): """ @@ -437,10 +423,17 @@ def __init__(self): sage: S.seen() # random [] """ + self._allow_external = True # For multiprocessing of doctests, the data self._seen should be # shared among subprocesses. Thus we use Array class from the # multiprocessing module. - self._seen = Array('i', len(external_software)) # initialized to zeroes + from sage.features.all import all_features + self._external_features = set(external_features()) + features = set(self._external_features) + features.update(all_features()) + self._features = sorted(features, key=lambda feature: feature.name) + self._indices = {feature.name: idx for idx, feature in enumerate(self._features)} + self._seen = Array('i', len(self._features)) # initialized to zeroes def __contains__(self, item): """ @@ -453,11 +446,13 @@ def __contains__(self, item): True """ try: - idx = external_software.index(item) - except Exception: + idx = self._indices[item] + except KeyError: return False if not self._seen[idx]: - if _lookup(item): + if not self._allow_external and self._features[idx] in self._external_features: + self._seen[idx] = -1 # not available + elif self._features[idx].is_present(): self._seen[idx] = 1 # available else: self._seen[idx] = -1 # not available @@ -493,6 +488,8 @@ def seen(self): sage: available_software.seen() # random ['internet', 'latex', 'magma'] """ - return [external_software[i] for i in range(len(external_software)) if self._seen[i] > 0] + return [feature.name + for feature, seen in zip(self._features, self._seen) + if seen > 0] available_software = AvailableSoftware() diff --git a/src/sage/doctest/parsing.py b/src/sage/doctest/parsing.py index 3346ca14a24..dbb8a8f43a9 100644 --- a/src/sage/doctest/parsing.py +++ b/src/sage/doctest/parsing.py @@ -798,8 +798,7 @@ def parse(self, string, *args): if self.optional_tags is not True: extra = optional_tags - self.optional_tags # set difference if extra: - if not('external' in self.optional_tags - and available_software.issuperset(extra)): + if not available_software.issuperset(extra): continue elif self.optional_only: self.optionals['sage'] += 1 From 68b3d4186903d2d215b709bb17c55979ac4353b1 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 14 Nov 2021 16:08:20 -0800 Subject: [PATCH 20/29] src/sage/doctest/external.py (external_features): Rewrite as generator, add doctest --- src/sage/doctest/external.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index 882227fdbe1..e95592b86ce 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -334,32 +334,40 @@ def has_4ti2(): return FourTi2().is_present() def external_features(): - features = [] + r""" + Generate the features that are only to be tested if ``--optional=external`` is used. + + EXAMPLES:: + + sage: from sage.doctest.external import external_features + sage: next(external_features()) + Feature('internet') + """ from sage.features.internet import Internet - features.append(Internet()) + yield Internet() import sage.features.latex - features.extend(sage.features.latex.all_features()) + yield from sage.features.latex.all_features() import sage.features.interfaces - features.extend(sage.features.interfaces.all_features()) + yield from sage.features.interfaces.all_features() from sage.features.mip_backends import CPLEX, Gurobi - features.extend([CPLEX(), Gurobi()]) + yield CPLEX() + yield Gurobi() from sage.features.pandoc import Pandoc - features.append(Pandoc()) + yield Pandoc() from sage.features.graphviz import Graphviz - features.append(Graphviz()) + yield Graphviz() from sage.features.ffmpeg import FFmpeg - features.append(FFmpeg()) + yield FFmpeg() from sage.features.imagemagick import ImageMagick - features.append(ImageMagick()) + yield ImageMagick() from sage.features.dvipng import dvipng - features.append(dvipng()) + yield dvipng() from sage.features.pdf2svg import pdf2svg - features.append(pdf2svg()) + yield pdf2svg() from sage.features.rubiks import Rubiks - features.append(Rubiks()) + yield Rubiks() from sage.features.four_ti_2 import FourTi2 - features.append(FourTi2()) - return features + yield FourTi2() def external_software(): """ From 6f2d22bebad1f9c043499bdc0a5fea6406ac6c7f Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Mon, 15 Nov 2021 09:47:40 -0800 Subject: [PATCH 21/29] src/sage/doctest/external.py: Declare pandoc, graphviz, ffmpeg, imagemagick, dvipng, pdf2svg, rubiks, 4ti2 feature tests safe (do not require 'external' to test them) --- src/sage/doctest/external.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index e95592b86ce..de69191a5e3 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -345,29 +345,11 @@ def external_features(): """ from sage.features.internet import Internet yield Internet() - import sage.features.latex - yield from sage.features.latex.all_features() import sage.features.interfaces yield from sage.features.interfaces.all_features() from sage.features.mip_backends import CPLEX, Gurobi yield CPLEX() yield Gurobi() - from sage.features.pandoc import Pandoc - yield Pandoc() - from sage.features.graphviz import Graphviz - yield Graphviz() - from sage.features.ffmpeg import FFmpeg - yield FFmpeg() - from sage.features.imagemagick import ImageMagick - yield ImageMagick() - from sage.features.dvipng import dvipng - yield dvipng() - from sage.features.pdf2svg import pdf2svg - yield pdf2svg() - from sage.features.rubiks import Rubiks - yield Rubiks() - from sage.features.four_ti_2 import FourTi2 - yield FourTi2() def external_software(): """ From 2f50385550e03e6f02d1432864be5dba559b081e Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Mon, 15 Nov 2021 10:04:25 -0800 Subject: [PATCH 22/29] src/sage/features/: Add more all_features functions --- src/sage/features/csdp.py | 4 ++++ src/sage/features/databases.py | 7 +++++++ src/sage/features/imagemagick.py | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/src/sage/features/csdp.py b/src/sage/features/csdp.py index 1d739f684eb..90fc8ddf4cc 100644 --- a/src/sage/features/csdp.py +++ b/src/sage/features/csdp.py @@ -64,3 +64,7 @@ def is_functional(self): .format(command=" ".join(command))) return FeatureTestResult(self, True) + + +def all_features(): + return [CSDP()] diff --git a/src/sage/features/databases.py b/src/sage/features/databases.py index 7fff8075914..afb9373cd15 100644 --- a/src/sage/features/databases.py +++ b/src/sage/features/databases.py @@ -125,3 +125,10 @@ def __init__(self): True """ PythonModule.__init__(self, 'database_knotinfo', spkg='database_knotinfo') + + +def all_features(): + return [DatabaseConwayPolynomials(), + DatabaseCremona(), DatabaseCremona('cremona_mini'), + DatabaseJones(), + DatabaseKnotInfo()] diff --git a/src/sage/features/imagemagick.py b/src/sage/features/imagemagick.py index 7ea27ea40de..8bf53a24f3a 100644 --- a/src/sage/features/imagemagick.py +++ b/src/sage/features/imagemagick.py @@ -46,3 +46,7 @@ def __init__(self): [Executable("convert", executable="convert")], spkg="_recommended", url="https://www.imagemagick.org/") + + +def all_features(): + return [ImageMagick()] From e2cda415abc4c5af6fea8e4739016d877414091e Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Mon, 15 Nov 2021 10:17:54 -0800 Subject: [PATCH 23/29] src/sage/features/: Add remaining missing all_features functions --- src/sage/features/ffmpeg.py | 4 ++++ src/sage/features/four_ti_2.py | 4 ++++ src/sage/features/graph_generators.py | 6 ++++++ src/sage/features/graphviz.py | 4 ++++ src/sage/features/internet.py | 4 ++++ src/sage/features/kenzo.py | 3 +++ src/sage/features/latte.py | 4 ++++ src/sage/features/lrs.py | 4 ++++ src/sage/features/mcqd.py | 4 ++++ src/sage/features/meataxe.py | 4 ++++ src/sage/features/pandoc.py | 4 ++++ src/sage/features/rubiks.py | 4 ++++ src/sage/features/tdlib.py | 4 ++++ 13 files changed, 53 insertions(+) diff --git a/src/sage/features/ffmpeg.py b/src/sage/features/ffmpeg.py index 68245d24c27..ebfda670951 100644 --- a/src/sage/features/ffmpeg.py +++ b/src/sage/features/ffmpeg.py @@ -35,3 +35,7 @@ def __init__(self): """ Executable.__init__(self, "ffmpeg", executable="ffmpeg", url="https://www.ffmpeg.org/") + + +def all_packages(): + return [FFmpeg()] diff --git a/src/sage/features/four_ti_2.py b/src/sage/features/four_ti_2.py index 83ac7b6d2ab..1eda0402607 100644 --- a/src/sage/features/four_ti_2.py +++ b/src/sage/features/four_ti_2.py @@ -44,3 +44,7 @@ def __init__(self): # same list is tested in build/pkgs/4ti2/spkg-configure.m4 for x in ('hilbert', 'markov', 'graver', 'zsolve', 'qsolve', 'rays', 'ppi', 'circuits', 'groebner')]) + + +def all_features(): + return [FourTi2()] diff --git a/src/sage/features/graph_generators.py b/src/sage/features/graph_generators.py index a47dca0e5d5..1fcd5764879 100644 --- a/src/sage/features/graph_generators.py +++ b/src/sage/features/graph_generators.py @@ -152,3 +152,9 @@ def is_functional(self): reason="Call `{command}` did not produce output that started with `{expected}`.".format(command=" ".join(command), expected=expected)) return FeatureTestResult(self, True) + + +def all_features(): + return [Plantri(), + Buckygen(), + Benzene()] diff --git a/src/sage/features/graphviz.py b/src/sage/features/graphviz.py index 8df3c8002bc..2eb284da424 100644 --- a/src/sage/features/graphviz.py +++ b/src/sage/features/graphviz.py @@ -110,3 +110,7 @@ def __init__(self): [dot(), neato(), twopi()], spkg="graphviz", url="https://www.graphviz.org/") + + +def all_features(): + return [Graphviz()] diff --git a/src/sage/features/internet.py b/src/sage/features/internet.py index 77fa6f0046b..992419e812a 100644 --- a/src/sage/features/internet.py +++ b/src/sage/features/internet.py @@ -44,3 +44,7 @@ def _is_present(self): return FeatureTestResult(self, True) except urllib.error.URLError: return FeatureTestResult(self, False) + + +def all_features(): + return [Internet()] diff --git a/src/sage/features/kenzo.py b/src/sage/features/kenzo.py index f1778d9a379..345f1ae388f 100644 --- a/src/sage/features/kenzo.py +++ b/src/sage/features/kenzo.py @@ -56,3 +56,6 @@ def _is_present(self): return FeatureTestResult(self, False, reason="Unable to make ECL require kenzo") return FeatureTestResult(self, True) + +def all_features(): + return [Kenzo()] diff --git a/src/sage/features/latte.py b/src/sage/features/latte.py index 7b4d9dda756..496719bd0c8 100644 --- a/src/sage/features/latte.py +++ b/src/sage/features/latte.py @@ -65,3 +65,7 @@ def __init__(self): JoinFeature.__init__(self, "latte_int", (Latte_count(), Latte_integrate()), description="LattE") + + +def all_features(): + return [Latte()] diff --git a/src/sage/features/lrs.py b/src/sage/features/lrs.py index ab1871a6314..f9deae3caf0 100644 --- a/src/sage/features/lrs.py +++ b/src/sage/features/lrs.py @@ -63,3 +63,7 @@ def is_functional(self): expected=" or ".join(expected_list))) return FeatureTestResult(self, True) + + +def all_features(): + return [Lrs()] diff --git a/src/sage/features/mcqd.py b/src/sage/features/mcqd.py index 5fe2f26e881..ec5809f384b 100644 --- a/src/sage/features/mcqd.py +++ b/src/sage/features/mcqd.py @@ -25,3 +25,7 @@ def __init__(self): # Will be changed to spkg='sagemath_mcqd' later JoinFeature.__init__(self, 'mcqd', [PythonModule('sage.graphs.mcqd', spkg='mcqd')]) + + +def all_features(): + return [Mcqd()] diff --git a/src/sage/features/meataxe.py b/src/sage/features/meataxe.py index eb32016ac3e..5bbefe1f0d1 100644 --- a/src/sage/features/meataxe.py +++ b/src/sage/features/meataxe.py @@ -24,3 +24,7 @@ def __init__(self): # Will be changed to spkg='sagemath_meataxe' later JoinFeature.__init__(self, 'meataxe', [PythonModule('sage.matrix.matrix_gfpn_dense', spkg='meataxe')]) + + +def all_features(): + return [Meataxe()] diff --git a/src/sage/features/pandoc.py b/src/sage/features/pandoc.py index 566032a643b..4532b49132d 100644 --- a/src/sage/features/pandoc.py +++ b/src/sage/features/pandoc.py @@ -35,3 +35,7 @@ def __init__(self): """ Executable.__init__(self, "pandoc", executable="pandoc", url="https://pandoc.org/") + + +def all_features(): + return [Pandoc()] diff --git a/src/sage/features/rubiks.py b/src/sage/features/rubiks.py index 9074fe58c81..6175374ed1b 100644 --- a/src/sage/features/rubiks.py +++ b/src/sage/features/rubiks.py @@ -174,3 +174,7 @@ def __init__(self): JoinFeature.__init__(self, "rubiks", [cu2(), size222(), optimal(), mcube(), dikcube(), cubex()], spkg="rubiks") + + +def all_features(): + return [Rubiks()] diff --git a/src/sage/features/tdlib.py b/src/sage/features/tdlib.py index cae1f5abc78..ad73807883c 100644 --- a/src/sage/features/tdlib.py +++ b/src/sage/features/tdlib.py @@ -18,3 +18,7 @@ def __init__(self): # Will be changed to spkg='sagemath_tdlib' later JoinFeature.__init__(self, 'tdlib', [PythonModule('sage.graphs.graph_decompositions.tdlib', spkg='tdlib')]) + + +def all_features(): + return [Tdlib()] From c4deceb2babdadafc3044bb5918c1f8a4467c82e Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Mon, 15 Nov 2021 10:43:51 -0800 Subject: [PATCH 24/29] src/sage/doctest/control.py: Replace use of 'external_software' by new method 'available_software.detectable()' --- src/sage/doctest/control.py | 4 ++-- src/sage/doctest/external.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py index 37f7f469d37..30bc01b143b 100644 --- a/src/sage/doctest/control.py +++ b/src/sage/doctest/control.py @@ -38,7 +38,7 @@ from .forker import DocTestDispatcher from .reporting import DocTestReporter from .util import Timer, count_noun, dict_difference -from .external import external_software, available_software +from .external import available_software from .parsing import parse_optional_tags nodoctest_regex = re.compile(r'\s*(#+|%+|r"+|"+|\.\.)\s*nodoctest') @@ -1242,8 +1242,8 @@ def run(self): pass self.log("Using --optional=" + self._optional_tags_string()) - self.log("External software to be detected: " + ','.join(external_software)) available_software._allow_external = self.options.optional is True or 'external' in self.options.optional + self.log("Features to be detected: " + ','.join(available_software.detectable())) self.add_files() self.expand_files_into_sources() self.filter_sources() diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index de69191a5e3..c2ad4ed4949 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -468,6 +468,14 @@ def issuperset(self, other): return False return True + def detectable(self): + """ + Return the list of names of those features for which testing their presence is allowed. + """ + return [feature.name + for feature in self._features + if self._allow_external or feature not in self._external_features] + def seen(self): """ Return the list of detected external software. @@ -482,4 +490,5 @@ def seen(self): for feature, seen in zip(self._features, self._seen) if seen > 0] + available_software = AvailableSoftware() From b8f0a107bd1bc621ecf5d7cacddae0e2110b8427 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Mon, 15 Nov 2021 14:26:57 -0800 Subject: [PATCH 25/29] src/sage/features/normaliz.py: Add all_features --- src/sage/features/normaliz.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sage/features/normaliz.py b/src/sage/features/normaliz.py index ceb67875b01..ae3879c1409 100644 --- a/src/sage/features/normaliz.py +++ b/src/sage/features/normaliz.py @@ -26,3 +26,7 @@ def __init__(self): """ JoinFeature.__init__(self, 'pynormaliz', [PythonModule('PyNormaliz', spkg="pynormaliz")]) + + +def all_features(): + return [PyNormaliz()] From 944bb74f0bf0260fbc262152005745edfcdd3ad9 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Mon, 15 Nov 2021 16:13:54 -0800 Subject: [PATCH 26/29] src/sage/features/interfaces.py: Add doctests --- src/sage/features/interfaces.py | 91 ++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/sage/features/interfaces.py b/src/sage/features/interfaces.py index 231fba136b2..9991cfc690c 100644 --- a/src/sage/features/interfaces.py +++ b/src/sage/features/interfaces.py @@ -8,6 +8,8 @@ class InterfaceFeature(Feature): r""" + A :class:`Feature` describing whether an :class:`~sage.interfaces.interface.Interface` is present and functional. + TESTS:: sage: from sage.features.interfaces import InterfaceFeature @@ -25,15 +27,41 @@ class InterfaceFeature(Feature): """ @staticmethod def __classcall__(cls, name, module, description=None): + """ + TESTS:: + + sage: from sage.features import PythonModule + sage: from sage.features.interfaces import InterfaceFeature + sage: f = InterfaceFeature("test_interface", "sage.interfaces.interface") + sage: f is InterfaceFeature("test_interface", PythonModule("sage.interfaces.interface")) + True + """ if isinstance(module, str): module = PythonModule(module) return Feature.__classcall__(cls, name, module, description) def __init__(self, name, module, description): + """ + TESTS:: + + sage: from sage.features.interfaces import InterfaceFeature + sage: f = InterfaceFeature("test_interface", "sage.interfaces.interface") + sage: isinstance(f, InterfaceFeature) + True + """ super().__init__(name, description=description) self.module = module def _is_present(self): + """ + TESTS:: + + sage: from sage.features.interfaces import InterfaceFeature + sage: from sage.interfaces.sage0 import Sage + sage: f = InterfaceFeature("sage0", "sage.interfaces.sage0") + sage: f.is_present() + FeatureTestResult('sage0', True) + """ result = self.module.is_present() if not result: return result @@ -50,6 +78,7 @@ def _is_present(self): return FeatureTestResult(self, False, reason=f"Interface {interface} is not functional: {exception}") + # The following are provided by external software only (no SPKG) class Magma(InterfaceFeature): @@ -61,7 +90,7 @@ class Magma(InterfaceFeature): sage: from sage.features.interfaces import Magma sage: Magma().is_present() # random - FeatureTestResult('jupymake', False) + FeatureTestResult('magma', False) """ @staticmethod @@ -70,6 +99,16 @@ def __classcall__(cls): class Matlab(InterfaceFeature): + r""" + A :class:`sage.features.Feature` describing whether :class:`sage.interfaces.matlab.Matlab` + is present and functional. + + EXAMPLES:: + + sage: from sage.features.interfaces import Matlab + sage: Matlab().is_present() # random + FeatureTestResult('matlab', False) + """ @staticmethod def __classcall__(cls): @@ -77,6 +116,16 @@ def __classcall__(cls): class Mathematica(InterfaceFeature): + r""" + A :class:`sage.features.Feature` describing whether :class:`sage.interfaces.mathematica.Mathematica` + is present and functional. + + EXAMPLES:: + + sage: from sage.features.interfaces import Mathematica + sage: Mathematica().is_present() # random + FeatureTestResult('mathematica', False) + """ @staticmethod def __classcall__(cls): @@ -84,6 +133,16 @@ def __classcall__(cls): class Maple(InterfaceFeature): + r""" + A :class:`sage.features.Feature` describing whether :class:`sage.interfaces.maple.Maple` + is present and functional. + + EXAMPLES:: + + sage: from sage.features.interfaces import Maple + sage: Maple().is_present() # random + FeatureTestResult('maple', False) + """ @staticmethod def __classcall__(cls): @@ -91,6 +150,16 @@ def __classcall__(cls): class Macaulay2(InterfaceFeature): + r""" + A :class:`sage.features.Feature` describing whether :class:`sage.interfaces.macaulay2.Macaulay2` + is present and functional. + + EXAMPLES:: + + sage: from sage.features.interfaces import Macaulay2 + sage: Macaulay2().is_present() # random + FeatureTestResult('macaulay2', False) + """ @staticmethod def __classcall__(cls): @@ -98,6 +167,16 @@ def __classcall__(cls): class Octave(InterfaceFeature): + r""" + A :class:`sage.features.Feature` describing whether :class:`sage.interfaces.octave.Octave` + is present and functional. + + EXAMPLES:: + + sage: from sage.features.interfaces import Octave + sage: Octave().is_present() # random + FeatureTestResult('octave', False) + """ @staticmethod def __classcall__(cls): @@ -105,6 +184,16 @@ def __classcall__(cls): class Scilab(InterfaceFeature): + r""" + A :class:`sage.features.Feature` describing whether :class:`sage.interfaces.scilab.Scilab` + is present and functional. + + EXAMPLES:: + + sage: from sage.features.interfaces import Scilab + sage: Scilab().is_present() # random + FeatureTestResult('scilab', False) + """ @staticmethod def __classcall__(cls): From 91f0bcc173d40c15abfba67d7c01a453726a1a02 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Mon, 15 Nov 2021 16:46:49 -0800 Subject: [PATCH 27/29] src/sage/doctest: Update doctest outputs --- src/sage/doctest/control.py | 9 ++++++--- src/sage/doctest/external.py | 16 ++-------------- src/sage/doctest/forker.py | 2 ++ 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py index 30bc01b143b..88bdb6e49b8 100644 --- a/src/sage/doctest/control.py +++ b/src/sage/doctest/control.py @@ -923,7 +923,7 @@ def run_doctests(self): ---------------------------------------------------------------------- Total time for all tests: ... seconds cpu time: ... seconds - cumulative wall time: ... seconds + cumulative wall time: ... seconds... """ nfiles = 0 nother = 0 @@ -999,6 +999,7 @@ def cleanup(self, final=True): Total time for all tests: ... seconds cpu time: ... seconds cumulative wall time: ... seconds + Features detected... 0 sage: DC.cleanup() """ @@ -1189,6 +1190,7 @@ def run(self): Total time for all tests: ... seconds cpu time: ... seconds cumulative wall time: ... seconds + Features detected... 0 We check that :trac:`25378` is fixed (testing external packages @@ -1202,7 +1204,7 @@ def run(self): sage: DC.run() Running doctests with ID ... Using --optional=external,sage - External software to be detected: ... + Features to be detected: ... Doctesting 1 file. sage -t ....py [0 tests, ... s] @@ -1212,7 +1214,7 @@ def run(self): Total time for all tests: ... seconds cpu time: ... seconds cumulative wall time: ... seconds - External software detected for doctesting:... + Features detected... 0 """ @@ -1279,6 +1281,7 @@ def run_doctests(module, options=None): Total time for all tests: ... seconds cpu time: ... seconds cumulative wall time: ... seconds + Features detected... """ import sys sys.stdout.flush() diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index c2ad4ed4949..ad78384c67a 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -375,28 +375,16 @@ class AvailableSoftware(object): sage: from sage.doctest.external import external_software, available_software sage: external_software - ['4ti2', - 'cplex', - 'dvipng', - 'ffmpeg', - 'graphviz', + ['cplex', 'gurobi', - 'imagemagick', 'internet', - 'latex', - 'lualatex', 'macaulay2', 'magma', 'maple', 'mathematica', 'matlab', 'octave', - 'pandoc', - 'pdf2svg', - 'pdflatex', - 'rubiks', - 'scilab', - 'xelatex'] + 'scilab'] sage: 'internet' in available_software # random, optional - internet True sage: available_software.issuperset(set(['internet','latex'])) # random, optional - internet latex diff --git a/src/sage/doctest/forker.py b/src/sage/doctest/forker.py index 3307d50939e..e36c7bdbafb 100644 --- a/src/sage/doctest/forker.py +++ b/src/sage/doctest/forker.py @@ -2056,6 +2056,7 @@ def __init__(self, source, options, funclist=[]): Total time for all tests: ... seconds cpu time: ... seconds cumulative wall time: ... seconds + Features detected... """ multiprocessing.Process.__init__(self) @@ -2101,6 +2102,7 @@ def run(self): Total time for all tests: ... seconds cpu time: ... seconds cumulative wall time: ... seconds + Features detected... """ os.setpgid(os.getpid(), os.getpid()) From 12f7c98f790982e4ed6ab4126d2b0c80efef8bf7 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Mon, 15 Nov 2021 17:00:38 -0800 Subject: [PATCH 28/29] src/sage/features/polymake.py: Add all_features --- src/sage/features/polymake.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sage/features/polymake.py b/src/sage/features/polymake.py index 1e6d7b81b99..cbcd8badc0e 100644 --- a/src/sage/features/polymake.py +++ b/src/sage/features/polymake.py @@ -23,3 +23,7 @@ def __init__(self): """ JoinFeature.__init__(self, "jupymake", [PythonModule("JuPyMake", spkg="jupymake")]) + + +def all_features(): + return [JuPyMake()] From c9312dbe74009795f2ea1a20cb5f4c2b087415c7 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Mon, 15 Nov 2021 17:06:02 -0800 Subject: [PATCH 29/29] src/sage/features: Add more all_features functions --- src/sage/features/dvipng.py | 4 ++++ src/sage/features/ffmpeg.py | 2 +- src/sage/features/pdf2svg.py | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/sage/features/dvipng.py b/src/sage/features/dvipng.py index 27c5bddc059..a22bdbf83b0 100644 --- a/src/sage/features/dvipng.py +++ b/src/sage/features/dvipng.py @@ -34,3 +34,7 @@ def __init__(self): """ Executable.__init__(self, "dvipng", executable="dvipng", url="https://savannah.nongnu.org/projects/dvipng/") + + +def all_features(): + return [dvipng()] diff --git a/src/sage/features/ffmpeg.py b/src/sage/features/ffmpeg.py index ebfda670951..ea26e38145d 100644 --- a/src/sage/features/ffmpeg.py +++ b/src/sage/features/ffmpeg.py @@ -37,5 +37,5 @@ def __init__(self): url="https://www.ffmpeg.org/") -def all_packages(): +def all_features(): return [FFmpeg()] diff --git a/src/sage/features/pdf2svg.py b/src/sage/features/pdf2svg.py index f0b634f5ff4..fc964611901 100644 --- a/src/sage/features/pdf2svg.py +++ b/src/sage/features/pdf2svg.py @@ -35,3 +35,7 @@ def __init__(self): Executable.__init__(self, "pdf2svg", executable="pdf2svg", spkg='pdf2svg', url="http://www.cityinthesky.co.uk/opensource/pdf2svg/") + + +def all_features(): + return [pdf2svg()]