Skip to content

Commit f69396c

Browse files
committed
Merge branch 'u/soehms/hide_features_34185' of https://github.com/sagemath/sagetrac-mirror into hide_features_34185
2 parents 6ba0eaf + 2a582f4 commit f69396c

File tree

6 files changed

+349
-14
lines changed

6 files changed

+349
-14
lines changed

src/bin/sage-runtests

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ if __name__ == "__main__":
5252
'if set to "all", then all tests will be run; '
5353
'use "!FEATURE" to disable tests marked "# optional - FEATURE". '
5454
'Note that "!" needs to be quoted or escaped in the shell.')
55+
parser.add_argument("--hide", metavar="FEATURES", default="",
56+
help='run tests pretending that the software listed in FEATURES (separated by commas) is not installed; '
57+
'if "all" is listed, will also hide features corresponding to all non standard packages; '
58+
'if "optional" is listed, will also hide features corresponding to optional packages.')
5559
parser.add_argument("--randorder", type=int, metavar="SEED", help="randomize order of tests")
5660
parser.add_argument("--random-seed", dest="random_seed", type=int, metavar="SEED", help="random seed (integer) for fuzzing doctests",
5761
default=os.environ.get("SAGE_DOCTEST_RANDOM_SEED"))

src/sage/doctest/control.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
except ImportError:
5858
pass
5959

60-
6160
class DocTestDefaults(SageObject):
6261
"""
6362
This class is used for doctesting the Sage doctest module.
@@ -137,6 +136,7 @@ def __init__(self, **kwds):
137136
# displaying user-defined optional tags and we don't want to see
138137
# the auto_optional_tags there.
139138
self.optional = set(['sage']) | auto_optional_tags
139+
self.hide = ''
140140

141141
# > 0: always run GC before every test
142142
# < 0: disable GC
@@ -401,6 +401,28 @@ def __init__(self, options, args):
401401
if options.verbose:
402402
options.show_skipped = True
403403

404+
options.hidden_features = set()
405+
if isinstance(options.hide, str):
406+
if not len(options.hide):
407+
options.hide = set([])
408+
else:
409+
s = options.hide.lower()
410+
options.hide = set(s.split(','))
411+
for h in options.hide:
412+
if not optionaltag_regex.search(h):
413+
raise ValueError('invalid optional tag {!r}'.format(h))
414+
if 'all' in options.hide:
415+
options.hide.discard('all')
416+
from sage.features.all import all_features
417+
feature_names = set([f.name for f in all_features() if not f.is_standard()])
418+
options.hide = options.hide.union(feature_names)
419+
if 'optional' in options.hide:
420+
options.hide.discard('optional')
421+
from sage.features.all import all_features
422+
feature_names = set([f.name for f in all_features() if f.is_optional()])
423+
options.hide = options.hide.union(feature_names)
424+
425+
404426
options.disabled_optional = set()
405427
if isinstance(options.optional, str):
406428
s = options.optional.lower()
@@ -417,6 +439,8 @@ def __init__(self, options, args):
417439
options.optional.discard('optional')
418440
from sage.misc.package import list_packages
419441
for pkg in list_packages('optional', local=True).values():
442+
if pkg.name in options.hide:
443+
continue
420444
if pkg.is_installed() and pkg.installed_version == pkg.remote_version:
421445
options.optional.add(pkg.name)
422446

@@ -1329,6 +1353,49 @@ def run(self):
13291353
Features detected...
13301354
0
13311355
1356+
We test the ``--hide`` option (:trac:`34185`):
1357+
1358+
sage: from sage.doctest.control import test_hide
1359+
sage: filename = tmp_filename(ext='.py')
1360+
sage: with open(filename, 'w') as f:
1361+
....: f.write(test_hide)
1362+
....: f.close()
1363+
729
1364+
sage: DF = DocTestDefaults(hide='buckygen,all')
1365+
sage: DC = DocTestController(DF, [filename])
1366+
sage: DC.run()
1367+
Running doctests with ID ...
1368+
Using --optional=sage...
1369+
Features to be detected: ...
1370+
Doctesting 1 file.
1371+
sage -t ....py
1372+
[4 tests, ... s]
1373+
----------------------------------------------------------------------
1374+
All tests passed!
1375+
----------------------------------------------------------------------
1376+
Total time for all tests: ... seconds
1377+
cpu time: ... seconds
1378+
cumulative wall time: ... seconds
1379+
Features detected...
1380+
0
1381+
1382+
sage: DF = DocTestDefaults(hide='benzene,optional')
1383+
sage: DC = DocTestController(DF, [filename])
1384+
sage: DC.run()
1385+
Running doctests with ID ...
1386+
Using --optional=sage
1387+
Features to be detected: ...
1388+
Doctesting 1 file.
1389+
sage -t ....py
1390+
[4 tests, ... s]
1391+
----------------------------------------------------------------------
1392+
All tests passed!
1393+
----------------------------------------------------------------------
1394+
Total time for all tests: ... seconds
1395+
cpu time: ... seconds
1396+
cumulative wall time: ... seconds
1397+
Features detected...
1398+
0
13321399
"""
13331400
opt = self.options
13341401
L = (opt.gdb, opt.lldb, opt.valgrind, opt.massif, opt.cachegrind, opt.omega)
@@ -1369,6 +1436,21 @@ def run(self):
13691436

13701437
self.log("Using --optional=" + self._optional_tags_string())
13711438
available_software._allow_external = self.options.optional is True or 'external' in self.options.optional
1439+
1440+
for h in self.options.hide:
1441+
try:
1442+
i = available_software._indices[h]
1443+
except KeyError:
1444+
pass
1445+
else:
1446+
f = available_software._features[i]
1447+
if f.is_present():
1448+
f.hide()
1449+
self.options.hidden_features.add(f)
1450+
for g in f.joined_features():
1451+
if g.name in self.options.optional:
1452+
self.options.optional.discard(g.name)
1453+
13721454
for o in self.options.disabled_optional:
13731455
try:
13741456
i = available_software._indices[o]
@@ -1378,12 +1460,17 @@ def run(self):
13781460
available_software._seen[i] = -1
13791461

13801462
self.log("Features to be detected: " + ','.join(available_software.detectable()))
1463+
if self.options.hidden_features:
1464+
self.log("Hidden features: " + ','.join([f.name for f in self.options.hidden_features]))
13811465
self.add_files()
13821466
self.expand_files_into_sources()
13831467
self.filter_sources()
13841468
self.sort_sources()
13851469
self.run_doctests()
13861470

1471+
for f in self.options.hidden_features:
1472+
f.unhide()
1473+
13871474
self.log("Features detected for doctesting: "
13881475
+ ','.join(available_software.seen()))
13891476
self.cleanup()
@@ -1464,3 +1551,34 @@ def stringify(x):
14641551
if not save_dtmode and IP is not None:
14651552
IP.run_line_magic('colors', old_color)
14661553
IP.config.TerminalInteractiveShell.colors = old_config_color
1554+
1555+
1556+
###############################################################################
1557+
# Declaration of doctest strings
1558+
###############################################################################
1559+
1560+
test_hide=r"""r{quotmark}
1561+
{prompt}: next(graphs.fullerenes(20))
1562+
Traceback (most recent call last):
1563+
...
1564+
FeatureNotPresentError: buckygen is not available.
1565+
...
1566+
{prompt}: next(graphs.fullerenes(20)) # optional buckygen
1567+
Graph on 20 vertices
1568+
1569+
{prompt}: len(list(graphs.fusenes(2)))
1570+
Traceback (most recent call last):
1571+
...
1572+
FeatureNotPresentError: benzene is not available.
1573+
...
1574+
{prompt}: len(list(graphs.fusenes(2))) # optional benzene
1575+
1
1576+
{prompt}: from sage.matrix.matrix_space import get_matrix_class
1577+
{prompt}: get_matrix_class(GF(25,'x'), 4, 4, False, 'meataxe')
1578+
Failed lazy import:
1579+
sage.matrix.matrix_gfpn_dense is not available.
1580+
...
1581+
{prompt}: get_matrix_class(GF(25,'x'), 4, 4, False, 'meataxe') # optional meataxe
1582+
<class 'sage.matrix.matrix_gfpn_dense.Matrix_gfpn_dense'>
1583+
{quotmark}
1584+
""".format(quotmark='"""', prompt='sage') # using prompt to hide these lines from _test_enough_doctests

src/sage/features/__init__.py

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ def __call__(cls, *args, **kwds):
8787
else:
8888
return type.__call__(cls, *args, **kwds)
8989

90+
9091
_trivial_unique_representation_cache = dict()
9192

93+
9294
class TrivialUniqueRepresentation(metaclass=TrivialClasscallMetaClass):
9395
r"""
9496
A trivial version of :class:`UniqueRepresentation` without Cython dependencies.
@@ -105,6 +107,7 @@ def __classcall__(cls, *args, **options):
105107
cached = _trivial_unique_representation_cache[key] = type.__call__(cls, *args, **options)
106108
return cached
107109

110+
108111
class Feature(TrivialUniqueRepresentation):
109112
r"""
110113
A feature of the runtime environment
@@ -150,6 +153,7 @@ def __init__(self, name, spkg=None, url=None, description=None):
150153

151154
self._cache_is_present = None
152155
self._cache_resolution = None
156+
self._hidden = False
153157

154158
def is_present(self):
155159
r"""
@@ -186,6 +190,8 @@ def is_present(self):
186190
sage: TestFeature("other").is_present()
187191
FeatureTestResult('other', True)
188192
"""
193+
if self._hidden:
194+
return FeatureTestResult(self, False, reason="Feature `{name}` is hidden.".format(name=self.name))
189195
# We do not use @cached_method here because we wish to use
190196
# Feature early in the build system of sagelib.
191197
if self._cache_is_present is None:
@@ -238,6 +244,27 @@ def __repr__(self):
238244
description = f'{self.name!r}: {self.description}' if self.description else f'{self.name!r}'
239245
return f'Feature({description})'
240246

247+
def _spkg_type(self):
248+
r"""
249+
Return the type of the SPKG corresponding to this feature.
250+
251+
EXAMPLES::
252+
253+
sage: from sage.features.databases import DatabaseCremona
254+
sage: DatabaseCremona()._spkg_type()
255+
'optional'
256+
257+
OUTPUT:
258+
259+
The type as a string in ``('base', 'standard', 'optional', 'experimental')``.
260+
If no SPKG corresponds to this feature ``None`` is returned.
261+
"""
262+
from sage.misc.package import _spkg_type
263+
spkg = self.spkg
264+
if not spkg:
265+
spkg = self.name
266+
return _spkg_type(spkg)
267+
241268
def resolution(self):
242269
r"""
243270
Return a suggestion on how to make :meth:`is_present` pass if it did not
@@ -253,6 +280,8 @@ def resolution(self):
253280
sage: Executable(name="CSDP", spkg="csdp", executable="theta", url="https://github.com/dimpase/csdp").resolution() # optional - sage_spkg
254281
'...To install CSDP...you can try to run...sage -i csdp...Further installation instructions might be available at https://github.com/dimpase/csdp.'
255282
"""
283+
if self._hidden:
284+
return "Use method `unhide` to make it available again."
256285
if self._cache_resolution is not None:
257286
return self._cache_resolution
258287
lines = []
@@ -264,6 +293,109 @@ def resolution(self):
264293
self._cache_resolution = "\n".join(lines)
265294
return self._cache_resolution
266295

296+
def joined_features(self):
297+
r"""
298+
Return a list of features joined with ``self``.
299+
300+
OUTPUT:
301+
302+
A (possibly empty) list of instances of :class:`Feature`.
303+
304+
EXAMPLES::
305+
306+
sage: from sage.features.graphviz import Graphviz
307+
sage: Graphviz().joined_features()
308+
[Feature('dot'), Feature('neato'), Feature('twopi')]
309+
sage: from sage.features.interfaces import Mathematica
310+
sage: Mathematica().joined_features()
311+
[]
312+
"""
313+
from sage.features.join_feature import JoinFeature
314+
if isinstance(self, JoinFeature):
315+
return self._features
316+
return []
317+
318+
def is_standard(self):
319+
r"""
320+
Return whether this feature corresponds to a standard SPKG.
321+
322+
EXAMPLES::
323+
324+
sage: from sage.features.databases import DatabaseCremona, DatabaseConwayPolynomials
325+
sage: DatabaseCremona().is_standard()
326+
False
327+
sage: DatabaseConwayPolynomials().is_standard()
328+
True
329+
"""
330+
if self.name.startswith('sage.'):
331+
return True
332+
return self._spkg_type() == 'standard'
333+
334+
def is_optional(self):
335+
r"""
336+
Return whether this feature corresponds to an optional SPKG.
337+
338+
EXAMPLES::
339+
340+
sage: from sage.features.databases import DatabaseCremona, DatabaseConwayPolynomials
341+
sage: DatabaseCremona().is_optional()
342+
True
343+
sage: DatabaseConwayPolynomials().is_optional()
344+
False
345+
"""
346+
return self._spkg_type() == 'optional'
347+
348+
def hide(self):
349+
r"""
350+
Hide this feature. For example this is used when the doctest option
351+
``--hide``is set. Setting an installed feature as hidden pretends
352+
that it is not available. To revert this use :meth:`unhide`.
353+
354+
EXAMPLES:
355+
356+
Benzene is an optional SPKG. The following test fails if it is hidden or
357+
not installed. Thus, in the second invocation the optional tag is needed::
358+
359+
sage: from sage.features.graph_generators import Benzene
360+
sage: Benzene().hide()
361+
sage: len(list(graphs.fusenes(2)))
362+
Traceback (most recent call last):
363+
...
364+
FeatureNotPresentError: benzene is not available.
365+
Feature `benzene` is hidden.
366+
Use method `unhide` to make it available again.
367+
368+
sage: Benzene().unhide()
369+
sage: len(list(graphs.fusenes(2))) # optional benzene
370+
1
371+
"""
372+
self._hidden = True
373+
374+
def unhide(self):
375+
r"""
376+
Revert what :meth:`hide` does.
377+
378+
EXAMPLES:
379+
380+
Polycyclic is a standard GAP package since 4.10 (see :trac:`26856`). The
381+
following test just fails if it is hidden. Thus, in the second
382+
invocation no optional tag is needed::
383+
384+
sage: from sage.features.gap import GapPackage
385+
sage: Polycyclic = GapPackage("polycyclic", spkg="gap_packages")
386+
sage: Polycyclic.hide()
387+
sage: libgap(AbelianGroup(3, [0,3,4], names="abc"))
388+
Traceback (most recent call last):
389+
...
390+
FeatureNotPresentError: gap_package_polycyclic is not available.
391+
Feature `gap_package_polycyclic` is hidden.
392+
Use method `unhide` to make it available again.
393+
394+
sage: Polycyclic.unhide()
395+
sage: libgap(AbelianGroup(3, [0,3,4], names="abc"))
396+
Pcp-group with orders [ 0, 3, 4 ]
397+
"""
398+
self._hidden = False
267399

268400
class FeatureNotPresentError(RuntimeError):
269401
r"""
@@ -682,7 +814,9 @@ def absolute_filename(self) -> str:
682814
A :class:`FeatureNotPresentError` is raised if the file cannot be found::
683815
684816
sage: from sage.features import StaticFile
685-
sage: StaticFile(name="no_such_file", filename="KaT1aihu", search_path=(), spkg="some_spkg", url="http://rand.om").absolute_filename() # optional - sage_spkg
817+
sage: StaticFile(name="no_such_file", filename="KaT1aihu",\
818+
search_path=(), spkg="some_spkg",\
819+
url="http://rand.om").absolute_filename() # optional - sage_spkg
686820
Traceback (most recent call last):
687821
...
688822
FeatureNotPresentError: no_such_file is not available.
@@ -694,9 +828,8 @@ def absolute_filename(self) -> str:
694828
path = os.path.join(directory, self.filename)
695829
if os.path.isfile(path) or os.path.isdir(path):
696830
return os.path.abspath(path)
697-
raise FeatureNotPresentError(self,
698-
reason="{filename!r} not found in any of {search_path}".format(filename=self.filename, search_path=self.search_path),
699-
resolution=self.resolution())
831+
reason = "{filename!r} not found in any of {search_path}".format(filename=self.filename, search_path=self.search_path)
832+
raise FeatureNotPresentError(self, reason=reason, resolution=self.resolution())
700833

701834

702835
class CythonFeature(Feature):

0 commit comments

Comments
 (0)