Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hide features (PR to migrated Trac ticket #34185) #35668

Merged
merged 25 commits into from
Jul 1, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f51f29a
34185: initial version
soehms Jul 15, 2022
3071dbc
34185: correction according to review
soehms Jul 18, 2022
faacd11
Merge branch 'u/soehms/hide_features_34185' of trac.sagemath.org:sage…
soehms Jul 24, 2022
b4b562c
34185: take care of joined features
soehms Jul 25, 2022
ddd7ada
Merge branch 'u/soehms/hide_features_34185' of trac.sagemath.org:sage…
soehms Aug 3, 2022
70abb3f
Merge branch 'u/soehms/hide_features_34185' of trac.sagemath.org:sage…
soehms Aug 4, 2022
442fca6
Merge branch 'join_feature_texfile_with_pdflatex_34282' into hide_fea…
soehms Aug 5, 2022
f39836a
Merge branch 'circular_import_matrix_space_34283' into hide_features_…
soehms Aug 5, 2022
0ec6d46
Merge branch 'u/soehms/hide_features_34185' of trac.sagemath.org:sage…
soehms Aug 7, 2022
f463ac7
Merge branch 'join_feature_texfile_with_pdflatex_34282' into hide_fea…
soehms Aug 9, 2022
ebbd4a4
Merge branch 'circular_import_matrix_space_34283' into hide_features_…
soehms Aug 9, 2022
6912c1c
34185: fix doctest failure + pep8 fixes
soehms Aug 9, 2022
2e11970
Merge branch 'u/soehms/hide_features_34185' of trac.sagemath.org:sage…
soehms Sep 3, 2022
d676f79
Merge branch 'u/soehms/join_feature_texfile_with_pdflatex_34282' of t…
soehms Sep 6, 2022
187bdcf
34185: fix typos
soehms Sep 6, 2022
2a582f4
Merge branch 'u/soehms/hide_features_34185' of trac.sagemath.org:sage…
soehms Sep 30, 2022
f69396c
Merge branch 'u/soehms/hide_features_34185' of https://github.com/sag…
soehms May 22, 2023
b695c04
34185: fix RST issues
soehms May 22, 2023
c3da4d8
34185 / 35668: fixes according to review
soehms May 25, 2023
6dff841
Merge branch 'sagemath:develop' into hide_features_34185
soehms May 25, 2023
72d9da7
Merge branch 'develop' into hide_features_34185
soehms May 30, 2023
7d58d67
Merge branch 'develop' into hide_features_34185
soehms Jun 11, 2023
560228c
Merge branch 'sagemath:develop' into hide_features_34185
soehms Jun 11, 2023
d0ccd37
Merge branch 'hide_features_34185' of github.com:soehms/sage into hid…
soehms Jun 19, 2023
97eaac9
35668: pycodestyle fix
soehms Jun 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/bin/sage-runtests
Original file line number Diff line number Diff line change
Expand Up @@ -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 non standard packages; '
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what "non standard" means here; do you mean "optional or experimental"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean "optional or experimental"?

Yes! I changed this, to have it more clear!

'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"))
Expand Down
120 changes: 119 additions & 1 deletion src/sage/doctest/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
except ImportError:
pass


class DocTestDefaults(SageObject):
"""
This class is used for doctesting the Sage doctest module.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -401,6 +401,28 @@ 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()
Expand All @@ -417,6 +439,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)

Expand Down Expand Up @@ -1329,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)
Expand Down Expand Up @@ -1369,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]
Expand All @@ -1378,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()
Expand Down Expand Up @@ -1464,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
<class 'sage.matrix.matrix_gfpn_dense.Matrix_gfpn_dense'>
{quotmark}
""".format(quotmark='"""', prompt='sage') # using prompt to hide these lines from _test_enough_doctests
141 changes: 137 additions & 4 deletions src/sage/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -150,6 +153,7 @@ def __init__(self, name, spkg=None, url=None, description=None):

self._cache_is_present = None
self._cache_resolution = None
self._hidden = False

def is_present(self):
r"""
Expand Down Expand Up @@ -186,6 +190,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:
Expand Down Expand Up @@ -238,6 +244,27 @@ 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 the SPKG corresponding to this feature.

EXAMPLES::

sage: from sage.features.databases import DatabaseCremona
sage: DatabaseCremona()._spkg_type()
'optional'

OUTPUT:

The type as a string in ``('base', 'standard', 'optional', 'experimental')``.
If no SPKG corresponds to this feature ``None`` is returned.
"""
from sage.misc.package import _spkg_type
spkg = self.spkg
if not spkg:
spkg = self.name
return _spkg_type(spkg)

def resolution(self):
r"""
Return a suggestion on how to make :meth:`is_present` pass if it did not
Expand All @@ -253,6 +280,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 = []
Expand All @@ -264,6 +293,109 @@ def resolution(self):
self._cache_resolution = "\n".join(lines)
return self._cache_resolution

def joined_features(self):
r"""
Return a list of features joined with ``self``.
mkoeppe marked this conversation as resolved.
Show resolved Hide resolved

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.interfaces import Mathematica
sage: Mathematica().joined_features()
[]
"""
from sage.features.join_feature import JoinFeature
if isinstance(self, JoinFeature):
return self._features
mkoeppe marked this conversation as resolved.
Show resolved Hide resolved
return []

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"""
Expand Down Expand Up @@ -682,7 +814,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.
Expand All @@ -694,9 +828,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):
Expand Down
Loading