diff --git a/src/bin/sage-runtests b/src/bin/sage-runtests index 6b92c980cfb..4131161647d 100755 --- a/src/bin/sage-runtests +++ b/src/bin/sage-runtests @@ -52,6 +52,10 @@ if __name__ == "__main__": 'if set to "all", then all tests will be run; ' 'use "!FEATURE" to disable tests marked "# optional - FEATURE". ' 'Note that "!" needs to be quoted or escaped in the shell.') + parser.add_argument("--hide", metavar="FEATURES", default="", + help='run tests pretending that the software listed in FEATURES (separated by commas) is not installed; ' + 'if "all" is listed, will also hide features corresponding to all optional or experimental packages; ' + 'if "optional" is listed, will also hide features corresponding to optional packages.') parser.add_argument("--randorder", type=int, metavar="SEED", help="randomize order of tests") parser.add_argument("--random-seed", dest="random_seed", type=int, metavar="SEED", help="random seed (integer) for fuzzing doctests", default=os.environ.get("SAGE_DOCTEST_RANDOM_SEED")) diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py index abaa33f6239..5aae3f020c6 100644 --- a/src/sage/doctest/control.py +++ b/src/sage/doctest/control.py @@ -57,7 +57,6 @@ except ImportError: pass - class DocTestDefaults(SageObject): """ This class is used for doctesting the Sage doctest module. @@ -137,6 +136,7 @@ def __init__(self, **kwds): # displaying user-defined optional tags and we don't want to see # the auto_optional_tags there. self.optional = set(['sage']) | auto_optional_tags + self.hide = '' # > 0: always run GC before every test # < 0: disable GC @@ -401,6 +401,27 @@ def __init__(self, options, args): if options.verbose: options.show_skipped = True + options.hidden_features = set() + if isinstance(options.hide, str): + if not len(options.hide): + options.hide = set([]) + else: + s = options.hide.lower() + options.hide = set(s.split(',')) + for h in options.hide: + if not optionaltag_regex.search(h): + raise ValueError('invalid optional tag {!r}'.format(h)) + if 'all' in options.hide: + options.hide.discard('all') + from sage.features.all import all_features + feature_names = set([f.name for f in all_features() if not f.is_standard()]) + options.hide = options.hide.union(feature_names) + if 'optional' in options.hide: + options.hide.discard('optional') + from sage.features.all import all_features + feature_names = set([f.name for f in all_features() if f.is_optional()]) + options.hide = options.hide.union(feature_names) + options.disabled_optional = set() if isinstance(options.optional, str): s = options.optional.lower() @@ -417,6 +438,8 @@ def __init__(self, options, args): options.optional.discard('optional') from sage.misc.package import list_packages for pkg in list_packages('optional', local=True).values(): + if pkg.name in options.hide: + continue if pkg.is_installed() and pkg.installed_version == pkg.remote_version: options.optional.add(pkg.name) @@ -1330,6 +1353,49 @@ def run(self): Features detected... 0 + We test the ``--hide`` option (:trac:`34185`):: + + sage: from sage.doctest.control import test_hide + sage: filename = tmp_filename(ext='.py') + sage: with open(filename, 'w') as f: + ....: f.write(test_hide) + ....: f.close() + 729 + sage: DF = DocTestDefaults(hide='buckygen,all') + sage: DC = DocTestController(DF, [filename]) + sage: DC.run() + Running doctests with ID ... + Using --optional=sage... + Features to be detected: ... + Doctesting 1 file. + sage -t ....py + [4 tests, ... s] + ---------------------------------------------------------------------- + All tests passed! + ---------------------------------------------------------------------- + Total time for all tests: ... seconds + cpu time: ... seconds + cumulative wall time: ... seconds + Features detected... + 0 + + sage: DF = DocTestDefaults(hide='benzene,optional') + sage: DC = DocTestController(DF, [filename]) + sage: DC.run() + Running doctests with ID ... + Using --optional=sage + Features to be detected: ... + Doctesting 1 file. + sage -t ....py + [4 tests, ... s] + ---------------------------------------------------------------------- + All tests passed! + ---------------------------------------------------------------------- + Total time for all tests: ... seconds + cpu time: ... seconds + cumulative wall time: ... seconds + Features detected... + 0 """ opt = self.options L = (opt.gdb, opt.lldb, opt.valgrind, opt.massif, opt.cachegrind, opt.omega) @@ -1370,6 +1436,21 @@ def run(self): self.log("Using --optional=" + self._optional_tags_string()) available_software._allow_external = self.options.optional is True or 'external' in self.options.optional + + for h in self.options.hide: + try: + i = available_software._indices[h] + except KeyError: + pass + else: + f = available_software._features[i] + if f.is_present(): + f.hide() + self.options.hidden_features.add(f) + for g in f.joined_features(): + if g.name in self.options.optional: + self.options.optional.discard(g.name) + for o in self.options.disabled_optional: try: i = available_software._indices[o] @@ -1379,12 +1460,17 @@ def run(self): available_software._seen[i] = -1 self.log("Features to be detected: " + ','.join(available_software.detectable())) + if self.options.hidden_features: + self.log("Hidden features: " + ','.join([f.name for f in self.options.hidden_features])) self.add_files() self.expand_files_into_sources() self.filter_sources() self.sort_sources() self.run_doctests() + for f in self.options.hidden_features: + f.unhide() + self.log("Features detected for doctesting: " + ','.join(available_software.seen())) self.cleanup() @@ -1465,3 +1551,34 @@ def stringify(x): if not save_dtmode and IP is not None: IP.run_line_magic('colors', old_color) IP.config.TerminalInteractiveShell.colors = old_config_color + + +############################################################################### +# Declaration of doctest strings +############################################################################### + +test_hide=r"""r{quotmark} +{prompt}: next(graphs.fullerenes(20)) +Traceback (most recent call last): + ... +FeatureNotPresentError: buckygen is not available. +... +{prompt}: next(graphs.fullerenes(20)) # optional buckygen +Graph on 20 vertices + +{prompt}: len(list(graphs.fusenes(2))) +Traceback (most recent call last): + ... +FeatureNotPresentError: benzene is not available. +... +{prompt}: len(list(graphs.fusenes(2))) # optional benzene +1 +{prompt}: from sage.matrix.matrix_space import get_matrix_class +{prompt}: get_matrix_class(GF(25,'x'), 4, 4, False, 'meataxe') +Failed lazy import: +sage.matrix.matrix_gfpn_dense is not available. +... +{prompt}: get_matrix_class(GF(25,'x'), 4, 4, False, 'meataxe') # optional meataxe + +{quotmark} +""".format(quotmark='"""', prompt='sage') # using prompt to hide these lines from _test_enough_doctests diff --git a/src/sage/features/__init__.py b/src/sage/features/__init__.py index 5f5f9affc5f..5deb0085a63 100644 --- a/src/sage/features/__init__.py +++ b/src/sage/features/__init__.py @@ -87,8 +87,10 @@ def __call__(cls, *args, **kwds): else: return type.__call__(cls, *args, **kwds) + _trivial_unique_representation_cache = dict() + class TrivialUniqueRepresentation(metaclass=TrivialClasscallMetaClass): r""" A trivial version of :class:`UniqueRepresentation` without Cython dependencies. @@ -105,6 +107,7 @@ def __classcall__(cls, *args, **options): cached = _trivial_unique_representation_cache[key] = type.__call__(cls, *args, **options) return cached + class Feature(TrivialUniqueRepresentation): r""" A feature of the runtime environment @@ -121,6 +124,8 @@ class Feature(TrivialUniqueRepresentation): - ``url`` -- a URL for the upstream package providing the feature + - ``type`` -- (string) one of ``'standard'``, ``'optional'`` (default), ``'experimental'`` + Overwrite :meth:`_is_present` to add feature checks. EXAMPLES:: @@ -134,7 +139,7 @@ class Feature(TrivialUniqueRepresentation): sage: GapPackage("grape") is GapPackage("grape") True """ - def __init__(self, name, spkg=None, url=None, description=None): + def __init__(self, name, spkg=None, url=None, description=None, type='optional'): r""" TESTS:: @@ -150,6 +155,20 @@ def __init__(self, name, spkg=None, url=None, description=None): self._cache_is_present = None self._cache_resolution = None + self._hidden = False + self._type = type + + try: + from sage.misc.package import spkg_type + except ImportError: # may have been surgically removed in a downstream distribution + pass + else: + if spkg and (t := spkg_type(spkg)) not in (type, None): + from warnings import warn + warn(f'Feature {name} is declared {type}, ' + f'but it is provided by {spkg}, ' + f'which is declared {t} in SAGE_ROOT/build/pkgs', + stacklevel=3) def is_present(self): r""" @@ -186,6 +205,8 @@ def is_present(self): sage: TestFeature("other").is_present() FeatureTestResult('other', True) """ + if self._hidden: + return FeatureTestResult(self, False, reason="Feature `{name}` is hidden.".format(name=self.name)) # We do not use @cached_method here because we wish to use # Feature early in the build system of sagelib. if self._cache_is_present is None: @@ -238,6 +259,25 @@ def __repr__(self): description = f'{self.name!r}: {self.description}' if self.description else f'{self.name!r}' return f'Feature({description})' + def _spkg_type(self): + r""" + Return the type of this feature. + + For features provided by an SPKG in the Sage distribution, + this should match the SPKG type, or a warning will be issued. + + EXAMPLES:: + + sage: from sage.features.databases import DatabaseCremona + sage: DatabaseCremona()._spkg_type() + 'optional' + + OUTPUT: + + The type as a string in ``('base', 'standard', 'optional', 'experimental')``. + """ + return self._type + def resolution(self): r""" Return a suggestion on how to make :meth:`is_present` pass if it did not @@ -253,6 +293,8 @@ def resolution(self): sage: Executable(name="CSDP", spkg="csdp", executable="theta", url="https://github.com/dimpase/csdp").resolution() # optional - sage_spkg '...To install CSDP...you can try to run...sage -i csdp...Further installation instructions might be available at https://github.com/dimpase/csdp.' """ + if self._hidden: + return "Use method `unhide` to make it available again." if self._cache_resolution is not None: return self._cache_resolution lines = [] @@ -264,6 +306,117 @@ def resolution(self): self._cache_resolution = "\n".join(lines) return self._cache_resolution + def joined_features(self): + r""" + Return a list of features that ``self`` is the join of. + + OUTPUT: + + A (possibly empty) list of instances of :class:`Feature`. + + EXAMPLES:: + + sage: from sage.features.graphviz import Graphviz + sage: Graphviz().joined_features() + [Feature('dot'), Feature('neato'), Feature('twopi')] + sage: from sage.features.sagemath import sage__rings__function_field + sage: sage__rings__function_field().joined_features() + [Feature('sage.rings.function_field.function_field_polymod'), + Feature('sage.libs.singular'), + Feature('sage.libs.singular.singular'), + Feature('sage.interfaces.singular')] + sage: from sage.features.interfaces import Mathematica + sage: Mathematica().joined_features() + [] + """ + from sage.features.join_feature import JoinFeature + res = [] + if isinstance(self, JoinFeature): + for f in self._features: + res += [f] + f.joined_features() + return res + + def is_standard(self): + r""" + Return whether this feature corresponds to a standard SPKG. + + EXAMPLES:: + + sage: from sage.features.databases import DatabaseCremona, DatabaseConwayPolynomials + sage: DatabaseCremona().is_standard() + False + sage: DatabaseConwayPolynomials().is_standard() + True + """ + if self.name.startswith('sage.'): + return True + return self._spkg_type() == 'standard' + + def is_optional(self): + r""" + Return whether this feature corresponds to an optional SPKG. + + EXAMPLES:: + + sage: from sage.features.databases import DatabaseCremona, DatabaseConwayPolynomials + sage: DatabaseCremona().is_optional() + True + sage: DatabaseConwayPolynomials().is_optional() + False + """ + return self._spkg_type() == 'optional' + + def hide(self): + r""" + Hide this feature. For example this is used when the doctest option + ``--hide`` is set. Setting an installed feature as hidden pretends + that it is not available. To revert this use :meth:`unhide`. + + EXAMPLES: + + Benzene is an optional SPKG. The following test fails if it is hidden or + not installed. Thus, in the second invocation the optional tag is needed:: + + sage: from sage.features.graph_generators import Benzene + sage: Benzene().hide() + sage: len(list(graphs.fusenes(2))) + Traceback (most recent call last): + ... + FeatureNotPresentError: benzene is not available. + Feature `benzene` is hidden. + Use method `unhide` to make it available again. + + sage: Benzene().unhide() + sage: len(list(graphs.fusenes(2))) # optional benzene + 1 + """ + self._hidden = True + + def unhide(self): + r""" + Revert what :meth:`hide` does. + + EXAMPLES: + + Polycyclic is a standard GAP package since 4.10 (see :trac:`26856`). The + following test just fails if it is hidden. Thus, in the second + invocation no optional tag is needed:: + + sage: from sage.features.gap import GapPackage + sage: Polycyclic = GapPackage("polycyclic", spkg="gap_packages") + sage: Polycyclic.hide() + sage: libgap(AbelianGroup(3, [0,3,4], names="abc")) + Traceback (most recent call last): + ... + FeatureNotPresentError: gap_package_polycyclic is not available. + Feature `gap_package_polycyclic` is hidden. + Use method `unhide` to make it available again. + + sage: Polycyclic.unhide() + sage: libgap(AbelianGroup(3, [0,3,4], names="abc")) + Pcp-group with orders [ 0, 3, 4 ] + """ + self._hidden = False class FeatureNotPresentError(RuntimeError): r""" @@ -682,7 +835,9 @@ def absolute_filename(self) -> str: A :class:`FeatureNotPresentError` is raised if the file cannot be found:: sage: from sage.features import StaticFile - sage: StaticFile(name="no_such_file", filename="KaT1aihu", search_path=(), spkg="some_spkg", url="http://rand.om").absolute_filename() # optional - sage_spkg + sage: StaticFile(name="no_such_file", filename="KaT1aihu",\ + search_path=(), spkg="some_spkg",\ + url="http://rand.om").absolute_filename() # optional - sage_spkg Traceback (most recent call last): ... FeatureNotPresentError: no_such_file is not available. @@ -694,9 +849,8 @@ def absolute_filename(self) -> str: path = os.path.join(directory, self.filename) if os.path.isfile(path) or os.path.isdir(path): return os.path.abspath(path) - raise FeatureNotPresentError(self, - reason="{filename!r} not found in any of {search_path}".format(filename=self.filename, search_path=self.search_path), - resolution=self.resolution()) + reason = "{filename!r} not found in any of {search_path}".format(filename=self.filename, search_path=self.search_path) + raise FeatureNotPresentError(self, reason=reason, resolution=self.resolution()) class CythonFeature(Feature): @@ -716,7 +870,7 @@ class CythonFeature(Feature): ....: ....: assert fabs(-1) == 1 ....: ''' - sage: fabs = CythonFeature("fabs", test_code=fabs_test_code, spkg="gcc", url="https://gnu.org") + sage: fabs = CythonFeature("fabs", test_code=fabs_test_code, spkg="gcc", url="https://gnu.org", type="standard") sage: fabs.is_present() FeatureTestResult('fabs', True) diff --git a/src/sage/features/cddlib.py b/src/sage/features/cddlib.py index b37f0304948..b8fdb6fed8e 100644 --- a/src/sage/features/cddlib.py +++ b/src/sage/features/cddlib.py @@ -34,4 +34,4 @@ def __init__(self, name='cddexec_gmp'): True """ Executable.__init__(self, name=name, executable=name, spkg="cddlib", - url="https://github.com/cddlib/cddlib") + url="https://github.com/cddlib/cddlib", type="standard") diff --git a/src/sage/features/databases.py b/src/sage/features/databases.py index 216dbb04af3..f9b297b1e30 100644 --- a/src/sage/features/databases.py +++ b/src/sage/features/databases.py @@ -52,7 +52,8 @@ def __init__(self): filename='conway_polynomials.p', search_path=search_path, spkg='conway_polynomials', - description="Frank Luebeck's database of Conway polynomials") + description="Frank Luebeck's database of Conway polynomials", + type='standard') CREMONA_DATA_DIRS = set([CREMONA_MINI_DATA_DIR, CREMONA_LARGE_DATA_DIR]) diff --git a/src/sage/features/gfan.py b/src/sage/features/gfan.py index 040ea64ec5d..a58090b4c91 100644 --- a/src/sage/features/gfan.py +++ b/src/sage/features/gfan.py @@ -30,7 +30,7 @@ def __init__(self, cmd=None): name = "gfan" else: name = f"gfan_{cmd}" - Executable.__init__(self, name, executable=name, spkg="gfan") + Executable.__init__(self, name, executable=name, spkg="gfan", type='standard') def all_features(): diff --git a/src/sage/features/join_feature.py b/src/sage/features/join_feature.py index d02ad669833..802cb433ba3 100644 --- a/src/sage/features/join_feature.py +++ b/src/sage/features/join_feature.py @@ -46,7 +46,9 @@ class JoinFeature(Feature): sage: F.is_present() FeatureTestResult('xxyyyy', False) """ - def __init__(self, name, features, spkg=None, url=None, description=None): + + def __init__(self, name, features, spkg=None, url=None, description=None, type=None, + **kwds): """ TESTS: @@ -68,7 +70,15 @@ def __init__(self, name, features, spkg=None, url=None, description=None): raise ValueError('given features have more than one url; provide url argument') elif len(urls) == 1: url = next(iter(urls)) - super().__init__(name, spkg=spkg, url=url, description=description) + if type is None: + if any(f._spkg_type() == 'experimental' for f in features): + type = 'experimental' + elif any(f._spkg_type() == 'optional' for f in features): + type = 'optional' + else: + type = 'standard' + + super().__init__(name, spkg=spkg, url=url, description=description, type=type, **kwds) self._features = features def _is_present(self): @@ -116,3 +126,50 @@ def is_functional(self): if not test: return test return FeatureTestResult(self, True) + + def hide(self): + r""" + Hide this feature and all its joined features. + + EXAMPLES:: + + sage: from sage.features.sagemath import sage__groups + sage: f = sage__groups() + sage: f.hide() + sage: f._features[0].is_present() + FeatureTestResult('sage.groups.perm_gps.permgroup', False) + + sage: f.require() + Traceback (most recent call last): + ... + FeatureNotPresentError: sage.groups is not available. + Feature `sage.groups` is hidden. + Use method `unhide` to make it available again. + """ + for f in self._features: + f.hide() + super(JoinFeature, self).hide() + + def unhide(self): + r""" + Revert what :meth:`hide` does. + + EXAMPLES:: + + sage: from sage.features.sagemath import sage__groups + sage: f = sage__groups() + sage: f.hide() + sage: f.is_present() + FeatureTestResult('sage.groups', False) + sage: f._features[0].is_present() + FeatureTestResult('sage.groups.perm_gps.permgroup', False) + + sage: f.unhide() + sage: f.is_present() # optional sage.groups + FeatureTestResult('sage.groups', True) + sage: f._features[0].is_present() # optional sage.groups + FeatureTestResult('sage.groups.perm_gps.permgroup', True) + """ + for f in self._features: + f.unhide() + super(JoinFeature, self).unhide() diff --git a/src/sage/features/mip_backends.py b/src/sage/features/mip_backends.py index d276e25abe8..98b4766c1f5 100644 --- a/src/sage/features/mip_backends.py +++ b/src/sage/features/mip_backends.py @@ -102,7 +102,8 @@ def __init__(self): JoinFeature.__init__(self, 'cvxopt', [MIPBackend('CVXOPT'), PythonModule('cvxopt')], - spkg='cvxopt') + spkg='cvxopt', + type='standard') def all_features(): diff --git a/src/sage/features/nauty.py b/src/sage/features/nauty.py index f57b5de2363..ebd2daeb311 100644 --- a/src/sage/features/nauty.py +++ b/src/sage/features/nauty.py @@ -37,7 +37,8 @@ def __init__(self, name): """ Executable.__init__(self, name=f"nauty_{name}", executable=f"{SAGE_NAUTY_BINS_PREFIX}{name}", - spkg="nauty") + spkg="nauty", + type="standard") class Nauty(JoinFeature): diff --git a/src/sage/features/palp.py b/src/sage/features/palp.py index 35758a0ecc7..b58a8fc2e54 100644 --- a/src/sage/features/palp.py +++ b/src/sage/features/palp.py @@ -36,11 +36,11 @@ def __init__(self, palpprog, suff=None): if suff: Executable.__init__(self, f"palp_{palpprog}_{suff}d", executable=f"{palpprog}-{suff}d.x", - spkg="palp") + spkg="palp", type="standard") else: Executable.__init__(self, f"palp_{palpprog}", executable=f"{palpprog}.x", - spkg="palp") + spkg="palp", type="standard") class Palp(JoinFeature): r""" diff --git a/src/sage/features/sagemath.py b/src/sage/features/sagemath.py index 969747b34e8..580be376237 100644 --- a/src/sage/features/sagemath.py +++ b/src/sage/features/sagemath.py @@ -40,7 +40,8 @@ def __init__(self): StaticFile.__init__(self, 'sagemath_doc_html', filename='html', search_path=(SAGE_DOC,), - spkg='sagemath_doc_html') + spkg='sagemath_doc_html', + type='standard') class sage__combinat(JoinFeature): @@ -67,7 +68,7 @@ def __init__(self): # Hence, we test a Python module within the package. JoinFeature.__init__(self, 'sage.combinat', [PythonModule('sage.combinat.tableau')], - spkg='sagemath_combinat') + spkg='sagemath_combinat', type="standard") class sage__geometry__polyhedron(PythonModule): @@ -90,7 +91,7 @@ def __init__(self): True """ PythonModule.__init__(self, 'sage.geometry.polyhedron', - spkg='sagemath_polyhedra') + spkg='sagemath_polyhedra', type="standard") class sage__graphs(JoinFeature): @@ -113,7 +114,7 @@ def __init__(self): """ JoinFeature.__init__(self, 'sage.graphs', [PythonModule('sage.graphs.graph')], - spkg='sagemath_graphs') + spkg='sagemath_graphs', type="standard") class sage__modular(JoinFeature): @@ -136,7 +137,7 @@ def __init__(self): """ JoinFeature.__init__(self, 'sage.modular', [PythonModule('sage.modular.modform.eisenstein_submodule')], - spkg='sagemath_schemes') + spkg='sagemath_schemes', type='standard') class sage__groups(JoinFeature): @@ -158,7 +159,8 @@ def __init__(self): True """ JoinFeature.__init__(self, 'sage.groups', - [PythonModule('sage.groups.perm_gps.permgroup')]) + [PythonModule('sage.groups.perm_gps.permgroup')], + spkg='sagemath_groups', type='standard') class sage__libs__flint(JoinFeature): @@ -183,7 +185,7 @@ def __init__(self): JoinFeature.__init__(self, 'sage.libs.flint', [PythonModule('sage.libs.flint.flint'), PythonModule('sage.libs.arb.arith')], - spkg='sagemath_flint') + spkg='sagemath_flint', type='standard') class sage__libs__ntl(JoinFeature): @@ -207,7 +209,7 @@ def __init__(self): """ JoinFeature.__init__(self, 'sage.libs.ntl', [PythonModule('sage.libs.ntl.convert')], - spkg='sagemath_ntl') + spkg='sagemath_ntl', type='standard') class sage__libs__pari(JoinFeature): @@ -230,7 +232,7 @@ def __init__(self): """ JoinFeature.__init__(self, 'sage.libs.pari', [PythonModule('sage.libs.pari.convert_sage')], - spkg='sagemath_pari') + spkg='sagemath_pari', type='standard') class sage__modules(JoinFeature): @@ -253,7 +255,7 @@ def __init__(self): """ JoinFeature.__init__(self, 'sage.modules', [PythonModule('sage.modules.free_module')], - spkg='sagemath_modules') + spkg='sagemath_modules', type='standard') class sage__plot(JoinFeature): @@ -276,7 +278,7 @@ def __init__(self): """ JoinFeature.__init__(self, 'sage.plot', [PythonModule('sage.plot.plot')], - spkg='sagemath_symbolics') + spkg='sagemath_symbolics', type='standard') class sage__rings__finite_rings(JoinFeature): @@ -299,7 +301,8 @@ def __init__(self): True """ JoinFeature.__init__(self, 'sage.rings.finite_rings', - [PythonModule('sage.rings.finite_rings.element_pari_ffelt')]) + [PythonModule('sage.rings.finite_rings.element_pari_ffelt')], + type='standard') class sage__rings__function_field(JoinFeature): @@ -322,7 +325,8 @@ def __init__(self): """ JoinFeature.__init__(self, 'sage.rings.function_field', [PythonModule('sage.rings.function_field.function_field_polymod'), - sage__libs__singular()]) + sage__libs__singular()], + type='standard') class sage__rings__number_field(JoinFeature): @@ -344,7 +348,8 @@ def __init__(self): True """ JoinFeature.__init__(self, 'sage.rings.number_field', - [PythonModule('sage.rings.number_field.number_field_element')]) + [PythonModule('sage.rings.number_field.number_field_element')], + type='standard') class sage__rings__padics(JoinFeature): @@ -366,7 +371,8 @@ def __init__(self): True """ JoinFeature.__init__(self, 'sage.rings.padics', - [PythonModule('sage.rings.padics.factory')]) + [PythonModule('sage.rings.padics.factory')], + type='standard') class sage__rings__polynomial__pbori(JoinFeature): @@ -389,7 +395,7 @@ def __init__(self): """ JoinFeature.__init__(self, 'sage.rings.polynomial.pbori', [PythonModule('sage.rings.polynomial.pbori.pbori')], - spkg='sagemath_brial') + spkg='sagemath_brial', type='standard') class sage__rings__real_double(PythonModule): diff --git a/src/sage/features/singular.py b/src/sage/features/singular.py index 5c83ff8a099..64a24320044 100644 --- a/src/sage/features/singular.py +++ b/src/sage/features/singular.py @@ -35,7 +35,7 @@ def __init__(self): True """ Executable.__init__(self, "singular", SINGULAR_BIN, - spkg='singular') + spkg='singular', type='standard') class sage__libs__singular(JoinFeature): diff --git a/src/sage/features/sphinx.py b/src/sage/features/sphinx.py index c83370cf28a..a70e8a11eee 100644 --- a/src/sage/features/sphinx.py +++ b/src/sage/features/sphinx.py @@ -35,7 +35,7 @@ def __init__(self): sage: isinstance(Sphinx(), Sphinx) True """ - PythonModule.__init__(self, 'sphinx', spkg='sphinx') + PythonModule.__init__(self, 'sphinx', spkg='sphinx', type='standard') def all_features(): diff --git a/src/sage/features/standard.py b/src/sage/features/standard.py index 54d1f804cef..c2090fc53a4 100644 --- a/src/sage/features/standard.py +++ b/src/sage/features/standard.py @@ -18,19 +18,19 @@ def all_features(): - return [PythonModule('cvxopt', spkg='cvxopt'), - PythonModule('fpylll', spkg='fpylll'), - JoinFeature('ipython', (PythonModule('IPython'),), spkg='ipython'), - JoinFeature('lrcalc_python', (PythonModule('lrcalc'),), spkg='lrcalc_python'), - PythonModule('mpmath', spkg='mpmath'), - PythonModule('networkx', spkg='networkx'), - PythonModule('numpy', spkg='numpy'), - PythonModule('pexpect', spkg='pexpect'), - JoinFeature('pillow', (PythonModule('PIL'),), spkg='pillow'), - JoinFeature('pplpy', (PythonModule('ppl'),), spkg='pplpy'), - PythonModule('primecountpy', spkg='primecountpy'), - PythonModule('ptyprocess', spkg='ptyprocess'), - PythonModule('pyparsing', spkg='pyparsing'), - PythonModule('requests', spkg='requests'), - PythonModule('scipy', spkg='scipy'), - PythonModule('sympy', spkg='sympy')] + return [PythonModule('cvxopt', spkg='cvxopt', type='standard'), + PythonModule('fpylll', spkg='fpylll', type='standard'), + JoinFeature('ipython', (PythonModule('IPython'),), spkg='ipython', type='standard'), + JoinFeature('lrcalc_python', (PythonModule('lrcalc'),), spkg='lrcalc_python', type='standard'), + PythonModule('mpmath', spkg='mpmath', type='standard'), + PythonModule('networkx', spkg='networkx', type='standard'), + PythonModule('numpy', spkg='numpy', type='standard'), + PythonModule('pexpect', spkg='pexpect', type='standard'), + JoinFeature('pillow', (PythonModule('PIL'),), spkg='pillow', type='standard'), + JoinFeature('pplpy', (PythonModule('ppl'),), spkg='pplpy', type='standard'), + PythonModule('primecountpy', spkg='primecountpy', type='standard'), + PythonModule('ptyprocess', spkg='ptyprocess', type='standard'), + PythonModule('pyparsing', spkg='pyparsing', type='standard'), + PythonModule('requests', spkg='requests', type='standard'), + PythonModule('scipy', spkg='scipy', type='standard'), + PythonModule('sympy', spkg='sympy', type='standard')] diff --git a/src/sage/geometry/cone.py b/src/sage/geometry/cone.py index 80c7dc63613..d37a444a564 100644 --- a/src/sage/geometry/cone.py +++ b/src/sage/geometry/cone.py @@ -237,9 +237,9 @@ from sage.features import PythonModule lazy_import('ppl', ['C_Polyhedron', 'Generator_System', 'Constraint_System', 'Linear_Expression', 'Poly_Con_Relation'], - feature=PythonModule("ppl", spkg="pplpy")) + feature=PythonModule("ppl", spkg="pplpy", type="standard")) lazy_import('ppl', ['ray', 'point'], as_=['PPL_ray', 'PPL_point'], - feature=PythonModule("ppl", spkg="pplpy")) + feature=PythonModule("ppl", spkg="pplpy", type="standard")) def is_Cone(x): diff --git a/src/sage/geometry/lattice_polytope.py b/src/sage/geometry/lattice_polytope.py index f7e4e69db63..921b0004dfe 100644 --- a/src/sage/geometry/lattice_polytope.py +++ b/src/sage/geometry/lattice_polytope.py @@ -119,9 +119,9 @@ from sage.features import PythonModule from sage.features.palp import PalpExecutable lazy_import('ppl', ['C_Polyhedron', 'Generator_System', 'Linear_Expression'], - feature=PythonModule("ppl", spkg="pplpy")) + feature=PythonModule("ppl", spkg="pplpy", type="standard")) lazy_import('ppl', 'point', as_='PPL_point', - feature=PythonModule("ppl", spkg="pplpy")) + feature=PythonModule("ppl", spkg="pplpy", type="standard")) from sage.matrix.constructor import matrix from sage.structure.element import is_Matrix diff --git a/src/sage/geometry/polyhedron/backend_ppl.py b/src/sage/geometry/polyhedron/backend_ppl.py index 791d1a4417b..a4012616deb 100644 --- a/src/sage/geometry/polyhedron/backend_ppl.py +++ b/src/sage/geometry/polyhedron/backend_ppl.py @@ -16,7 +16,7 @@ from sage.features import PythonModule lazy_import('ppl', ['C_Polyhedron', 'Generator_System', 'Constraint_System', 'Linear_Expression', 'line', 'ray', 'point'], - feature=PythonModule("ppl", spkg="pplpy")) + feature=PythonModule("ppl", spkg="pplpy", type="standard")) ######################################################################### diff --git a/src/sage/groups/braid.py b/src/sage/groups/braid.py index 67f1f8e4dd9..f4ad632b332 100644 --- a/src/sage/groups/braid.py +++ b/src/sage/groups/braid.py @@ -93,7 +93,7 @@ ['leftnormalform', 'rightnormalform', 'centralizer', 'supersummitset', 'greatestcommondivisor', 'leastcommonmultiple', 'conjugatingbraid', 'ultrasummitset', 'thurston_type', 'rigidity', 'sliding_circuits'], - feature=PythonModule('sage.libs.braiding', spkg='libbraiding')) + feature=PythonModule('sage.libs.braiding', spkg='libbraiding', type='standard')) class Braid(FiniteTypeArtinGroupElement): diff --git a/src/sage/misc/lazy_import.pyx b/src/sage/misc/lazy_import.pyx index ada787afa22..3ab404ffd02 100644 --- a/src/sage/misc/lazy_import.pyx +++ b/src/sage/misc/lazy_import.pyx @@ -257,15 +257,20 @@ cdef class LazyImport(): if finish_startup_called: warn(f"Option ``at_startup=True`` for lazy import {self._name} not needed anymore") + feature = self._feature try: self._object = getattr(__import__(self._module, {}, {}, [self._name]), self._name) except ImportError as e: - if self._feature: + if feature: # Avoid warnings from static type checkers by explicitly importing FeatureNotPresentError. from sage.features import FeatureNotPresentError - raise FeatureNotPresentError(self._feature, reason=f'Importing {self._name} failed: {e}') + raise FeatureNotPresentError(feature, reason=f'Importing {self._name} failed: {e}') raise + if feature: + # for the case that the feature is hidden + feature.require() + if self._deprecation is not None: from sage.misc.superseded import deprecation_cython as deprecation try: @@ -1084,7 +1089,7 @@ def lazy_import(module, names, as_=None, *, An example of an import relying on a feature:: sage: from sage.features import PythonModule - sage: lazy_import('ppl', 'equation', feature=PythonModule('ppl', spkg='pplpy')) + sage: lazy_import('ppl', 'equation', feature=PythonModule('ppl', spkg='pplpy', type='standard')) sage: equation sage: lazy_import('PyNormaliz', 'NmzListConeProperties', feature=PythonModule('PyNormaliz', spkg='pynormaliz')) # optional - pynormaliz diff --git a/src/sage/misc/package.py b/src/sage/misc/package.py index d0b5808e215..110f6b36d7b 100644 --- a/src/sage/misc/package.py +++ b/src/sage/misc/package.py @@ -125,6 +125,42 @@ def pip_remote_version(pkg, pypi_url=DEFAULT_PYPI, ignore_URLError=False): return max(stable_releases) +def spkg_type(name): + r""" + Return the type of the Sage package with the given name. + + INPUT: + + - ``name`` -- string giving the subdirectory name of the package under + ``SAGE_PKGS`` + + EXAMPLES:: + + sage: from sage.misc.package import spkg_type + sage: spkg_type('pip') # optional - sage_spkg + 'standard' + + OUTPUT: + + The type as a string in ``('base', 'standard', 'optional', 'experimental')``. + If no ``SPKG`` exists with the given name (or the directory ``SAGE_PKGS`` is + not avaialble), ``None`` is returned. + """ + spkg_type = None + from sage.env import SAGE_PKGS + if not SAGE_PKGS: + return None + try: + f = open(os.path.join(SAGE_PKGS, name, "type")) + except IOError: + # Probably an empty directory => ignore + return None + + with f: + spkg_type = f.read().strip() + return spkg_type + + def pip_installed_packages(normalization=None): r""" Return a dictionary `name->version` of installed pip packages. @@ -310,15 +346,10 @@ def list_packages(*pkg_types: str, pkg_sources: List[str] = ['normal', 'pip', 's for p in lp: - try: - f = open(os.path.join(SAGE_PKGS, p, "type")) - except IOError: - # Probably an empty directory => ignore + typ = spkg_type(p) + if not typ: continue - with f: - typ = f.read().strip() - if os.path.isfile(os.path.join(SAGE_PKGS, p, "requirements.txt")): src = 'pip' elif os.path.isfile(os.path.join(SAGE_PKGS, p, "checksums.ini")):