passagemath-environment 10.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- passagemath_environment-10.4.1.data/scripts/sage +1140 -0
- passagemath_environment-10.4.1.data/scripts/sage-env +667 -0
- passagemath_environment-10.4.1.data/scripts/sage-num-threads.py +105 -0
- passagemath_environment-10.4.1.data/scripts/sage-python +2 -0
- passagemath_environment-10.4.1.data/scripts/sage-venv-config +42 -0
- passagemath_environment-10.4.1.data/scripts/sage-version.sh +9 -0
- passagemath_environment-10.4.1.dist-info/METADATA +76 -0
- passagemath_environment-10.4.1.dist-info/RECORD +70 -0
- passagemath_environment-10.4.1.dist-info/WHEEL +5 -0
- passagemath_environment-10.4.1.dist-info/top_level.txt +1 -0
- sage/all__sagemath_environment.py +4 -0
- sage/env.py +496 -0
- sage/features/__init__.py +981 -0
- sage/features/all.py +126 -0
- sage/features/bliss.py +85 -0
- sage/features/cddlib.py +38 -0
- sage/features/coxeter3.py +45 -0
- sage/features/csdp.py +83 -0
- sage/features/cython.py +38 -0
- sage/features/databases.py +302 -0
- sage/features/dvipng.py +40 -0
- sage/features/ecm.py +42 -0
- sage/features/ffmpeg.py +119 -0
- sage/features/four_ti_2.py +55 -0
- sage/features/fricas.py +66 -0
- sage/features/gap.py +86 -0
- sage/features/gfan.py +38 -0
- sage/features/giac.py +30 -0
- sage/features/graph_generators.py +171 -0
- sage/features/graphviz.py +117 -0
- sage/features/igraph.py +44 -0
- sage/features/imagemagick.py +138 -0
- sage/features/interfaces.py +256 -0
- sage/features/internet.py +65 -0
- sage/features/jmol.py +44 -0
- sage/features/join_feature.py +146 -0
- sage/features/kenzo.py +77 -0
- sage/features/latex.py +300 -0
- sage/features/latte.py +85 -0
- sage/features/lrs.py +164 -0
- sage/features/mcqd.py +45 -0
- sage/features/meataxe.py +46 -0
- sage/features/mip_backends.py +114 -0
- sage/features/msolve.py +68 -0
- sage/features/nauty.py +70 -0
- sage/features/normaliz.py +43 -0
- sage/features/palp.py +65 -0
- sage/features/pandoc.py +42 -0
- sage/features/pdf2svg.py +41 -0
- sage/features/phitigra.py +42 -0
- sage/features/pkg_systems.py +195 -0
- sage/features/polymake.py +43 -0
- sage/features/poppler.py +58 -0
- sage/features/rubiks.py +180 -0
- sage/features/sagemath.py +1205 -0
- sage/features/sat.py +103 -0
- sage/features/singular.py +48 -0
- sage/features/sirocco.py +45 -0
- sage/features/sphinx.py +71 -0
- sage/features/standard.py +38 -0
- sage/features/symengine_py.py +44 -0
- sage/features/tdlib.py +38 -0
- sage/features/threejs.py +75 -0
- sage/features/topcom.py +67 -0
- sage/misc/all__sagemath_environment.py +2 -0
- sage/misc/package.py +570 -0
- sage/misc/package_dir.py +621 -0
- sage/misc/temporary_file.py +546 -0
- sage/misc/viewer.py +369 -0
- sage/version.py +5 -0
@@ -0,0 +1,981 @@
|
|
1
|
+
# sage_setup: distribution = sagemath-environment
|
2
|
+
r"""
|
3
|
+
Testing for features of the environment at runtime
|
4
|
+
|
5
|
+
A computation can require a certain package to be installed in the runtime
|
6
|
+
environment. Abstractly such a package describes a :class:`Feature` which can
|
7
|
+
be tested for at runtime. It can be of various kinds, most prominently an
|
8
|
+
:class:`Executable` in the ``PATH``, a :class:`PythonModule`, or an additional
|
9
|
+
package for some installed
|
10
|
+
system such as a :class:`~sage.features.gap.GapPackage`.
|
11
|
+
|
12
|
+
AUTHORS:
|
13
|
+
|
14
|
+
- Julian Rüth (2016-04-07): Initial version
|
15
|
+
|
16
|
+
- Jeroen Demeyer (2018-02-12): Refactoring and clean up
|
17
|
+
|
18
|
+
EXAMPLES:
|
19
|
+
|
20
|
+
Some generic features are available for common cases. For example, to
|
21
|
+
test for the existence of a binary, one can use an :class:`Executable`
|
22
|
+
feature::
|
23
|
+
|
24
|
+
sage: from sage.features import Executable
|
25
|
+
sage: Executable(name='sh', executable='sh').is_present()
|
26
|
+
FeatureTestResult('sh', True)
|
27
|
+
|
28
|
+
Here we test whether the grape GAP package is available::
|
29
|
+
|
30
|
+
sage: from sage.features.gap import GapPackage
|
31
|
+
sage: GapPackage("grape", spkg='gap_packages').is_present() # optional - gap_package_grape
|
32
|
+
FeatureTestResult('gap_package_grape', True)
|
33
|
+
|
34
|
+
Note that a :class:`FeatureTestResult` acts like a bool in most contexts::
|
35
|
+
|
36
|
+
sage: if Executable(name='sh', executable='sh').is_present(): "present."
|
37
|
+
'present.'
|
38
|
+
|
39
|
+
When one wants to raise an error if the feature is not available, one
|
40
|
+
can use the ``require`` method::
|
41
|
+
|
42
|
+
sage: Executable(name='sh', executable='sh').require()
|
43
|
+
|
44
|
+
sage: Executable(name='random', executable='randomOochoz6x', spkg='random', url='http://rand.om').require() # optional - sage_spkg
|
45
|
+
Traceback (most recent call last):
|
46
|
+
...
|
47
|
+
FeatureNotPresentError: random is not available.
|
48
|
+
Executable 'randomOochoz6x' not found on PATH.
|
49
|
+
...try to run...sage -i random...
|
50
|
+
Further installation instructions might be available at http://rand.om.
|
51
|
+
|
52
|
+
As can be seen above, features try to produce helpful error messages.
|
53
|
+
"""
|
54
|
+
|
55
|
+
# *****************************************************************************
|
56
|
+
# Copyright (C) 2016 Julian Rüth
|
57
|
+
# 2018 Jeroen Demeyer
|
58
|
+
# 2018 Timo Kaufmann
|
59
|
+
# 2019-2022 Matthias Koeppe
|
60
|
+
# 2021 Kwankyu Lee
|
61
|
+
#
|
62
|
+
# Distributed under the terms of the GNU General Public License (GPL)
|
63
|
+
# as published by the Free Software Foundation; either version 2 of
|
64
|
+
# the License, or (at your option) any later version.
|
65
|
+
# https://www.gnu.org/licenses/
|
66
|
+
# *****************************************************************************
|
67
|
+
|
68
|
+
from __future__ import annotations
|
69
|
+
|
70
|
+
import os
|
71
|
+
import shutil
|
72
|
+
from pathlib import Path
|
73
|
+
|
74
|
+
from sage.env import SAGE_SHARE, SAGE_LOCAL, SAGE_VENV
|
75
|
+
|
76
|
+
|
77
|
+
class TrivialClasscallMetaClass(type):
|
78
|
+
"""
|
79
|
+
A trivial version of :class:`sage.misc.classcall_metaclass.ClasscallMetaclass` without Cython dependencies.
|
80
|
+
"""
|
81
|
+
def __call__(cls, *args, **kwds):
|
82
|
+
r"""
|
83
|
+
This method implements ``cls(<some arguments>)``.
|
84
|
+
"""
|
85
|
+
if hasattr(cls, '__classcall__'):
|
86
|
+
return cls.__classcall__(cls, *args, **kwds)
|
87
|
+
else:
|
88
|
+
return type.__call__(cls, *args, **kwds)
|
89
|
+
|
90
|
+
|
91
|
+
_trivial_unique_representation_cache = dict()
|
92
|
+
|
93
|
+
|
94
|
+
class TrivialUniqueRepresentation(metaclass=TrivialClasscallMetaClass):
|
95
|
+
r"""
|
96
|
+
A trivial version of :class:`UniqueRepresentation` without Cython dependencies.
|
97
|
+
"""
|
98
|
+
|
99
|
+
@staticmethod
|
100
|
+
def __classcall__(cls, *args, **options):
|
101
|
+
r"""
|
102
|
+
Construct a new object of this class or reuse an existing one.
|
103
|
+
"""
|
104
|
+
key = (cls, tuple(args), frozenset(options.items()))
|
105
|
+
cached = _trivial_unique_representation_cache.get(key, None)
|
106
|
+
if cached is None:
|
107
|
+
cached = _trivial_unique_representation_cache[key] = type.__call__(cls, *args, **options)
|
108
|
+
return cached
|
109
|
+
|
110
|
+
|
111
|
+
class Feature(TrivialUniqueRepresentation):
|
112
|
+
r"""
|
113
|
+
A feature of the runtime environment.
|
114
|
+
|
115
|
+
INPUT:
|
116
|
+
|
117
|
+
- ``name`` -- string; name of the feature. This should be suitable as an optional tag
|
118
|
+
for the Sage doctester, i.e., lowercase alphanumeric with underscores (``_``) allowed;
|
119
|
+
features that correspond to Python modules/packages may use periods (``.``)
|
120
|
+
|
121
|
+
- ``spkg`` -- string; name of the SPKG providing the feature
|
122
|
+
|
123
|
+
- ``description`` -- string (optional); plain English description of the feature
|
124
|
+
|
125
|
+
- ``url`` -- a URL for the upstream package providing the feature
|
126
|
+
|
127
|
+
- ``type`` -- string; one of ``'standard'``, ``'optional'`` (default), ``'experimental'``
|
128
|
+
|
129
|
+
Overwrite :meth:`_is_present` to add feature checks.
|
130
|
+
|
131
|
+
EXAMPLES::
|
132
|
+
|
133
|
+
sage: from sage.features.gap import GapPackage
|
134
|
+
sage: GapPackage("grape", spkg='gap_packages') # indirect doctest
|
135
|
+
Feature('gap_package_grape')
|
136
|
+
|
137
|
+
For efficiency, features are unique::
|
138
|
+
|
139
|
+
sage: GapPackage("grape") is GapPackage("grape")
|
140
|
+
True
|
141
|
+
"""
|
142
|
+
def __init__(self, name, spkg=None, url=None, description=None, type='optional'):
|
143
|
+
r"""
|
144
|
+
TESTS::
|
145
|
+
|
146
|
+
sage: from sage.features import Feature
|
147
|
+
sage: from sage.features.gap import GapPackage
|
148
|
+
sage: isinstance(GapPackage("grape", spkg='gap_packages'), Feature) # indirect doctest
|
149
|
+
True
|
150
|
+
"""
|
151
|
+
self.name = name
|
152
|
+
self.spkg = spkg
|
153
|
+
self.url = url
|
154
|
+
self.description = description
|
155
|
+
|
156
|
+
self._cache_is_present = None
|
157
|
+
self._cache_resolution = None
|
158
|
+
self._hidden = False
|
159
|
+
self._type = type
|
160
|
+
|
161
|
+
try:
|
162
|
+
from sage.misc.package import spkg_type
|
163
|
+
except ImportError: # may have been surgically removed in a downstream distribution
|
164
|
+
pass
|
165
|
+
else:
|
166
|
+
if spkg and (t := spkg_type(spkg)) not in (type, None):
|
167
|
+
from warnings import warn
|
168
|
+
warn(f'Feature {name} is declared {type}, '
|
169
|
+
f'but it is provided by {spkg}, '
|
170
|
+
f'which is declared {t} in SAGE_ROOT/build/pkgs',
|
171
|
+
stacklevel=3)
|
172
|
+
|
173
|
+
def is_present(self):
|
174
|
+
r"""
|
175
|
+
Return whether the feature is present.
|
176
|
+
|
177
|
+
OUTPUT:
|
178
|
+
|
179
|
+
A :class:`FeatureTestResult` which can be used as a boolean and
|
180
|
+
contains additional information about the feature test.
|
181
|
+
|
182
|
+
EXAMPLES::
|
183
|
+
|
184
|
+
sage: from sage.features.gap import GapPackage
|
185
|
+
sage: GapPackage("grape", spkg='gap_packages').is_present() # optional - gap_package_grape
|
186
|
+
FeatureTestResult('gap_package_grape', True)
|
187
|
+
sage: GapPackage("NOT_A_PACKAGE", spkg='gap_packages').is_present()
|
188
|
+
FeatureTestResult('gap_package_NOT_A_PACKAGE', False)
|
189
|
+
|
190
|
+
The result is cached::
|
191
|
+
|
192
|
+
sage: from sage.features import Feature
|
193
|
+
sage: class TestFeature(Feature):
|
194
|
+
....: def _is_present(self):
|
195
|
+
....: print("checking presence")
|
196
|
+
....: return True
|
197
|
+
sage: TestFeature("test").is_present()
|
198
|
+
checking presence
|
199
|
+
FeatureTestResult('test', True)
|
200
|
+
sage: TestFeature("test").is_present()
|
201
|
+
FeatureTestResult('test', True)
|
202
|
+
sage: TestFeature("other").is_present()
|
203
|
+
checking presence
|
204
|
+
FeatureTestResult('other', True)
|
205
|
+
sage: TestFeature("other").is_present()
|
206
|
+
FeatureTestResult('other', True)
|
207
|
+
"""
|
208
|
+
# We do not use @cached_method here because we wish to use
|
209
|
+
# Feature early in the build system of sagelib.
|
210
|
+
if self._cache_is_present is None:
|
211
|
+
res = self._is_present()
|
212
|
+
if not isinstance(res, FeatureTestResult):
|
213
|
+
res = FeatureTestResult(self, res)
|
214
|
+
self._cache_is_present = res
|
215
|
+
|
216
|
+
if self._hidden:
|
217
|
+
return FeatureTestResult(self, False, reason="Feature `{name}` is hidden.".format(name=self.name))
|
218
|
+
|
219
|
+
return self._cache_is_present
|
220
|
+
|
221
|
+
def _is_present(self):
|
222
|
+
r"""
|
223
|
+
Override this in a derived class to implement the feature check.
|
224
|
+
|
225
|
+
This should return either an instance of
|
226
|
+
:class:`FeatureTestResult` or a boolean.
|
227
|
+
"""
|
228
|
+
raise NotImplementedError("_is_present not implemented for feature {!r}".format(self.name))
|
229
|
+
|
230
|
+
def require(self):
|
231
|
+
r"""
|
232
|
+
Raise a :exc:`FeatureNotPresentError` if the feature is not present.
|
233
|
+
|
234
|
+
EXAMPLES::
|
235
|
+
|
236
|
+
sage: from sage.features.gap import GapPackage
|
237
|
+
sage: GapPackage("ve1EeThu").require() # needs sage.libs.gap
|
238
|
+
Traceback (most recent call last):
|
239
|
+
...
|
240
|
+
FeatureNotPresentError: gap_package_ve1EeThu is not available.
|
241
|
+
`LoadPackage("ve1EeThu")` evaluated to `fail` in GAP.
|
242
|
+
"""
|
243
|
+
presence = self.is_present()
|
244
|
+
if not presence:
|
245
|
+
raise FeatureNotPresentError(self, presence.reason, presence.resolution)
|
246
|
+
|
247
|
+
def __repr__(self):
|
248
|
+
r"""
|
249
|
+
Return a printable representation of this object.
|
250
|
+
|
251
|
+
EXAMPLES::
|
252
|
+
|
253
|
+
sage: from sage.features.gap import GapPackage
|
254
|
+
sage: GapPackage("grape") # indirect doctest
|
255
|
+
Feature('gap_package_grape')
|
256
|
+
"""
|
257
|
+
description = f'{self.name!r}: {self.description}' if self.description else f'{self.name!r}'
|
258
|
+
return f'Feature({description})'
|
259
|
+
|
260
|
+
def _spkg_type(self):
|
261
|
+
r"""
|
262
|
+
Return the type of this feature.
|
263
|
+
|
264
|
+
For features provided by an SPKG in the Sage distribution,
|
265
|
+
this should match the SPKG type, or a warning will be issued.
|
266
|
+
|
267
|
+
EXAMPLES::
|
268
|
+
|
269
|
+
sage: from sage.features.databases import DatabaseCremona
|
270
|
+
sage: DatabaseCremona()._spkg_type()
|
271
|
+
'optional'
|
272
|
+
|
273
|
+
OUTPUT:
|
274
|
+
|
275
|
+
The type as a string in ``('base', 'standard', 'optional', 'experimental')``.
|
276
|
+
"""
|
277
|
+
return self._type
|
278
|
+
|
279
|
+
def resolution(self):
|
280
|
+
r"""
|
281
|
+
Return a suggestion on how to make :meth:`is_present` pass if it did not
|
282
|
+
pass.
|
283
|
+
|
284
|
+
OUTPUT: string
|
285
|
+
|
286
|
+
EXAMPLES::
|
287
|
+
|
288
|
+
sage: from sage.features import Executable
|
289
|
+
sage: Executable(name='CSDP', spkg='csdp', executable='theta', url='https://github.com/dimpase/csdp').resolution() # optional - sage_spkg
|
290
|
+
'...To install CSDP...you can try to run...sage -i csdp...Further installation instructions might be available at https://github.com/dimpase/csdp.'
|
291
|
+
"""
|
292
|
+
if self._hidden:
|
293
|
+
return "Use method `unhide` to make it available again."
|
294
|
+
if self._cache_resolution is not None:
|
295
|
+
return self._cache_resolution
|
296
|
+
lines = []
|
297
|
+
if self.spkg:
|
298
|
+
for ps in package_systems():
|
299
|
+
lines.append(ps.spkg_installation_hint(self.spkg, feature=self.name))
|
300
|
+
if self.url:
|
301
|
+
lines.append("Further installation instructions might be available at {url}.".format(url=self.url))
|
302
|
+
self._cache_resolution = "\n".join(lines)
|
303
|
+
return self._cache_resolution
|
304
|
+
|
305
|
+
def joined_features(self):
|
306
|
+
r"""
|
307
|
+
Return a list of features that ``self`` is the join of.
|
308
|
+
|
309
|
+
OUTPUT:
|
310
|
+
|
311
|
+
A (possibly empty) list of instances of :class:`Feature`.
|
312
|
+
|
313
|
+
EXAMPLES::
|
314
|
+
|
315
|
+
sage: from sage.features.graphviz import Graphviz
|
316
|
+
sage: Graphviz().joined_features()
|
317
|
+
[Feature('dot'), Feature('neato'), Feature('twopi')]
|
318
|
+
sage: from sage.features.sagemath import sage__rings__function_field
|
319
|
+
sage: sage__rings__function_field().joined_features()
|
320
|
+
[Feature('sage.rings.function_field.function_field_polymod'),
|
321
|
+
Feature('sage.libs.singular'),
|
322
|
+
Feature('sage.libs.singular.singular'),
|
323
|
+
Feature('sage.interfaces.singular')]
|
324
|
+
sage: from sage.features.interfaces import Mathematica
|
325
|
+
sage: Mathematica().joined_features()
|
326
|
+
[]
|
327
|
+
"""
|
328
|
+
from sage.features.join_feature import JoinFeature
|
329
|
+
res = []
|
330
|
+
if isinstance(self, JoinFeature):
|
331
|
+
for f in self._features:
|
332
|
+
res += [f] + f.joined_features()
|
333
|
+
return res
|
334
|
+
|
335
|
+
def is_standard(self):
|
336
|
+
r"""
|
337
|
+
Return whether this feature corresponds to a standard SPKG.
|
338
|
+
|
339
|
+
EXAMPLES::
|
340
|
+
|
341
|
+
sage: from sage.features.databases import DatabaseCremona
|
342
|
+
sage: DatabaseCremona().is_standard()
|
343
|
+
False
|
344
|
+
"""
|
345
|
+
if self.name.startswith('sage.'):
|
346
|
+
return True
|
347
|
+
return self._spkg_type() == 'standard'
|
348
|
+
|
349
|
+
def is_optional(self):
|
350
|
+
r"""
|
351
|
+
Return whether this feature corresponds to an optional SPKG.
|
352
|
+
|
353
|
+
EXAMPLES::
|
354
|
+
|
355
|
+
sage: from sage.features.databases import DatabaseCremona
|
356
|
+
sage: DatabaseCremona().is_optional()
|
357
|
+
True
|
358
|
+
"""
|
359
|
+
return self._spkg_type() == 'optional'
|
360
|
+
|
361
|
+
def hide(self):
|
362
|
+
r"""
|
363
|
+
Hide this feature. For example this is used when the doctest option
|
364
|
+
``--hide`` is set. Setting an installed feature as hidden pretends
|
365
|
+
that it is not available. To revert this use :meth:`unhide`.
|
366
|
+
|
367
|
+
EXAMPLES:
|
368
|
+
|
369
|
+
Benzene is an optional SPKG. The following test fails if it is hidden or
|
370
|
+
not installed. Thus, in the second invocation the optional tag is needed::
|
371
|
+
|
372
|
+
sage: from sage.features.graph_generators import Benzene
|
373
|
+
sage: Benzene().hide()
|
374
|
+
sage: len(list(graphs.fusenes(2))) # needs sage.graphs
|
375
|
+
Traceback (most recent call last):
|
376
|
+
...
|
377
|
+
FeatureNotPresentError: benzene is not available.
|
378
|
+
Feature `benzene` is hidden.
|
379
|
+
Use method `unhide` to make it available again.
|
380
|
+
|
381
|
+
sage: Benzene().unhide() # optional - benzene, needs sage.graphs
|
382
|
+
sage: len(list(graphs.fusenes(2))) # optional - benzene, needs sage.graphs
|
383
|
+
1
|
384
|
+
"""
|
385
|
+
self._hidden = True
|
386
|
+
|
387
|
+
def unhide(self):
|
388
|
+
r"""
|
389
|
+
Revert what :meth:`hide` did.
|
390
|
+
|
391
|
+
EXAMPLES:
|
392
|
+
|
393
|
+
sage: from sage.features.sagemath import sage__plot
|
394
|
+
sage: sage__plot().hide()
|
395
|
+
sage: sage__plot().is_present()
|
396
|
+
FeatureTestResult('sage.plot', False)
|
397
|
+
sage: sage__plot().unhide() # needs sage.plot
|
398
|
+
sage: sage__plot().is_present() # needs sage.plot
|
399
|
+
FeatureTestResult('sage.plot', True)
|
400
|
+
"""
|
401
|
+
self._hidden = False
|
402
|
+
|
403
|
+
def is_hidden(self):
|
404
|
+
r"""
|
405
|
+
Return whether ``self`` is present but currently hidden.
|
406
|
+
|
407
|
+
EXAMPLES:
|
408
|
+
|
409
|
+
sage: from sage.features.sagemath import sage__plot
|
410
|
+
sage: sage__plot().hide()
|
411
|
+
sage: sage__plot().is_hidden() # needs sage.plot
|
412
|
+
True
|
413
|
+
sage: sage__plot().unhide()
|
414
|
+
sage: sage__plot().is_hidden()
|
415
|
+
False
|
416
|
+
"""
|
417
|
+
if self._hidden and self._is_present():
|
418
|
+
return True
|
419
|
+
return False
|
420
|
+
|
421
|
+
class FeatureNotPresentError(RuntimeError):
|
422
|
+
r"""
|
423
|
+
A missing feature error.
|
424
|
+
|
425
|
+
EXAMPLES::
|
426
|
+
|
427
|
+
sage: from sage.features import Feature, FeatureTestResult
|
428
|
+
sage: class Missing(Feature):
|
429
|
+
....: def _is_present(self):
|
430
|
+
....: return False
|
431
|
+
|
432
|
+
sage: Missing(name='missing').require()
|
433
|
+
Traceback (most recent call last):
|
434
|
+
...
|
435
|
+
FeatureNotPresentError: missing is not available.
|
436
|
+
"""
|
437
|
+
def __init__(self, feature, reason=None, resolution=None):
|
438
|
+
self.feature = feature
|
439
|
+
self.reason = reason
|
440
|
+
self._resolution = resolution
|
441
|
+
|
442
|
+
@property
|
443
|
+
def resolution(self):
|
444
|
+
if self._resolution:
|
445
|
+
return self._resolution
|
446
|
+
return self.feature.resolution()
|
447
|
+
|
448
|
+
def __str__(self):
|
449
|
+
r"""
|
450
|
+
Return the error message.
|
451
|
+
|
452
|
+
EXAMPLES::
|
453
|
+
|
454
|
+
sage: from sage.features.gap import GapPackage
|
455
|
+
sage: GapPackage("gapZuHoh8Uu").require() # indirect doctest # needs sage.libs.gap
|
456
|
+
Traceback (most recent call last):
|
457
|
+
...
|
458
|
+
FeatureNotPresentError: gap_package_gapZuHoh8Uu is not available.
|
459
|
+
`LoadPackage("gapZuHoh8Uu")` evaluated to `fail` in GAP.
|
460
|
+
"""
|
461
|
+
lines = ["{feature} is not available.".format(feature=self.feature.name)]
|
462
|
+
if self.reason:
|
463
|
+
lines.append(self.reason)
|
464
|
+
resolution = self.resolution
|
465
|
+
if resolution:
|
466
|
+
lines.append(str(resolution))
|
467
|
+
return "\n".join(lines)
|
468
|
+
|
469
|
+
|
470
|
+
class FeatureTestResult():
|
471
|
+
r"""
|
472
|
+
The result of a :meth:`Feature.is_present` call.
|
473
|
+
|
474
|
+
Behaves like a boolean with some extra data which may explain why a feature
|
475
|
+
is not present and how this may be resolved.
|
476
|
+
|
477
|
+
EXAMPLES::
|
478
|
+
|
479
|
+
sage: from sage.features.gap import GapPackage
|
480
|
+
sage: presence = GapPackage("NOT_A_PACKAGE").is_present(); presence # indirect doctest
|
481
|
+
FeatureTestResult('gap_package_NOT_A_PACKAGE', False)
|
482
|
+
sage: bool(presence)
|
483
|
+
False
|
484
|
+
|
485
|
+
Explanatory messages might be available as ``reason`` and
|
486
|
+
``resolution``::
|
487
|
+
|
488
|
+
sage: presence.reason # needs sage.libs.gap
|
489
|
+
'`LoadPackage("NOT_A_PACKAGE")` evaluated to `fail` in GAP.'
|
490
|
+
sage: bool(presence.resolution)
|
491
|
+
False
|
492
|
+
|
493
|
+
If a feature is not present, ``resolution`` defaults to
|
494
|
+
``feature.resolution()`` if this is defined. If you do not want to use this
|
495
|
+
default you need explicitly set ``resolution`` to a string::
|
496
|
+
|
497
|
+
sage: from sage.features import FeatureTestResult
|
498
|
+
sage: package = GapPackage("NOT_A_PACKAGE", spkg='no_package')
|
499
|
+
sage: str(FeatureTestResult(package, True).resolution) # optional - sage_spkg
|
500
|
+
'...To install gap_package_NOT_A_PACKAGE...you can try to run...sage -i no_package...'
|
501
|
+
sage: str(FeatureTestResult(package, False).resolution) # optional - sage_spkg
|
502
|
+
'...To install gap_package_NOT_A_PACKAGE...you can try to run...sage -i no_package...'
|
503
|
+
sage: FeatureTestResult(package, False, resolution='rtm').resolution
|
504
|
+
'rtm'
|
505
|
+
"""
|
506
|
+
def __init__(self, feature, is_present, reason=None, resolution=None):
|
507
|
+
r"""
|
508
|
+
TESTS::
|
509
|
+
|
510
|
+
sage: from sage.features import Executable, FeatureTestResult
|
511
|
+
sage: isinstance(Executable(name='sh', executable='sh').is_present(), FeatureTestResult)
|
512
|
+
True
|
513
|
+
"""
|
514
|
+
self.feature = feature
|
515
|
+
self.is_present = is_present
|
516
|
+
self.reason = reason
|
517
|
+
self._resolution = resolution
|
518
|
+
|
519
|
+
@property
|
520
|
+
def resolution(self):
|
521
|
+
if self._resolution:
|
522
|
+
return self._resolution
|
523
|
+
return self.feature.resolution()
|
524
|
+
|
525
|
+
def __bool__(self):
|
526
|
+
r"""
|
527
|
+
Whether the tested :class:`Feature` is present.
|
528
|
+
|
529
|
+
TESTS::
|
530
|
+
|
531
|
+
sage: from sage.features import Feature, FeatureTestResult
|
532
|
+
sage: bool(FeatureTestResult(Feature("SomePresentFeature"), True)) # indirect doctest
|
533
|
+
True
|
534
|
+
sage: bool(FeatureTestResult(Feature("SomeMissingFeature"), False))
|
535
|
+
False
|
536
|
+
"""
|
537
|
+
return bool(self.is_present)
|
538
|
+
|
539
|
+
def __repr__(self):
|
540
|
+
r"""
|
541
|
+
TESTS::
|
542
|
+
|
543
|
+
sage: from sage.features import Feature, FeatureTestResult
|
544
|
+
sage: FeatureTestResult(Feature("SomePresentFeature"), True) # indirect doctest
|
545
|
+
FeatureTestResult('SomePresentFeature', True)
|
546
|
+
"""
|
547
|
+
return "FeatureTestResult({feature!r}, {is_present!r})".format(feature=self.feature.name, is_present=self.is_present)
|
548
|
+
|
549
|
+
|
550
|
+
_cache_package_systems = None
|
551
|
+
|
552
|
+
|
553
|
+
def package_systems():
|
554
|
+
"""
|
555
|
+
Return a list of :class:`~sage.features.pkg_systems.PackageSystem` objects
|
556
|
+
representing the available package systems.
|
557
|
+
|
558
|
+
The list is ordered by decreasing preference.
|
559
|
+
|
560
|
+
EXAMPLES::
|
561
|
+
|
562
|
+
sage: from sage.features import package_systems
|
563
|
+
sage: package_systems() # random
|
564
|
+
[Feature('homebrew'), Feature('sage_spkg'), Feature('pip')]
|
565
|
+
"""
|
566
|
+
# The current implementation never returns more than one system.
|
567
|
+
from subprocess import run, CalledProcessError, PIPE
|
568
|
+
global _cache_package_systems
|
569
|
+
if _cache_package_systems is None:
|
570
|
+
from .pkg_systems import PackageSystem, SagePackageSystem, PipPackageSystem
|
571
|
+
_cache_package_systems = []
|
572
|
+
# Try to use scripts from SAGE_ROOT (or an installation of sage_bootstrap)
|
573
|
+
# to obtain system package advice.
|
574
|
+
try:
|
575
|
+
proc = run('sage-guess-package-system', shell=True, capture_output=True, text=True, check=True)
|
576
|
+
system_name = proc.stdout.strip()
|
577
|
+
if system_name != 'unknown':
|
578
|
+
_cache_package_systems = [PackageSystem(system_name)]
|
579
|
+
except CalledProcessError:
|
580
|
+
pass
|
581
|
+
more_package_systems = [SagePackageSystem(), PipPackageSystem()]
|
582
|
+
_cache_package_systems += [ps for ps in more_package_systems if ps.is_present()]
|
583
|
+
|
584
|
+
return _cache_package_systems
|
585
|
+
|
586
|
+
|
587
|
+
class FileFeature(Feature):
|
588
|
+
r"""
|
589
|
+
Base class for features that describe a file or directory in the file system.
|
590
|
+
|
591
|
+
A subclass should implement a method :meth:`absolute_filename`.
|
592
|
+
|
593
|
+
EXAMPLES:
|
594
|
+
|
595
|
+
Two direct concrete subclasses of :class:`FileFeature` are defined::
|
596
|
+
|
597
|
+
sage: from sage.features import StaticFile, Executable, FileFeature
|
598
|
+
sage: issubclass(StaticFile, FileFeature)
|
599
|
+
True
|
600
|
+
sage: issubclass(Executable, FileFeature)
|
601
|
+
True
|
602
|
+
|
603
|
+
To work with the file described by the feature, use the method :meth:`absolute_filename`.
|
604
|
+
A :exc:`FeatureNotPresentError` is raised if the file cannot be found::
|
605
|
+
|
606
|
+
sage: Executable(name='does-not-exist', executable='does-not-exist-xxxxyxyyxyy').absolute_filename()
|
607
|
+
Traceback (most recent call last):
|
608
|
+
...
|
609
|
+
sage.features.FeatureNotPresentError: does-not-exist is not available.
|
610
|
+
Executable 'does-not-exist-xxxxyxyyxyy' not found on PATH.
|
611
|
+
|
612
|
+
A :class:`FileFeature` also provides the :meth:`is_present` method to test for
|
613
|
+
the presence of the file at run time. This is inherited from the base class
|
614
|
+
:class:`Feature`::
|
615
|
+
|
616
|
+
sage: Executable(name='sh', executable='sh').is_present()
|
617
|
+
FeatureTestResult('sh', True)
|
618
|
+
"""
|
619
|
+
def _is_present(self):
|
620
|
+
r"""
|
621
|
+
Whether the file is present.
|
622
|
+
|
623
|
+
EXAMPLES::
|
624
|
+
|
625
|
+
sage: from sage.features import StaticFile
|
626
|
+
sage: StaticFile(name='no_such_file', filename='KaT1aihu', spkg='some_spkg', url='http://rand.om').is_present()
|
627
|
+
FeatureTestResult('no_such_file', False)
|
628
|
+
"""
|
629
|
+
try:
|
630
|
+
abspath = self.absolute_filename()
|
631
|
+
return FeatureTestResult(self, True, reason="Found at `{abspath}`.".format(abspath=abspath))
|
632
|
+
except FeatureNotPresentError as e:
|
633
|
+
return FeatureTestResult(self, False, reason=e.reason, resolution=e.resolution)
|
634
|
+
|
635
|
+
def absolute_filename(self) -> str:
|
636
|
+
r"""
|
637
|
+
The absolute path of the file as a string.
|
638
|
+
|
639
|
+
Concrete subclasses must override this abstract method.
|
640
|
+
|
641
|
+
TESTS::
|
642
|
+
|
643
|
+
sage: from sage.features import FileFeature
|
644
|
+
sage: FileFeature(name='abstract_file').absolute_filename()
|
645
|
+
Traceback (most recent call last):
|
646
|
+
...
|
647
|
+
NotImplementedError
|
648
|
+
"""
|
649
|
+
# We do not use sage.misc.abstract_method here because that is provided by
|
650
|
+
# the distribution sagemath-objects, which is not an install-requires of
|
651
|
+
# the distribution sagemath-environment.
|
652
|
+
raise NotImplementedError
|
653
|
+
|
654
|
+
|
655
|
+
class Executable(FileFeature):
|
656
|
+
r"""
|
657
|
+
A feature describing an executable in the ``PATH``.
|
658
|
+
|
659
|
+
In an installation of Sage with ``SAGE_LOCAL`` different from ``SAGE_VENV``, the
|
660
|
+
executable is searched first in ``SAGE_VENV/bin``, then in ``SAGE_LOCAL/bin``,
|
661
|
+
then in ``PATH``.
|
662
|
+
|
663
|
+
.. NOTE::
|
664
|
+
|
665
|
+
Overwrite :meth:`is_functional` if you also want to check whether
|
666
|
+
the executable shows proper behaviour.
|
667
|
+
|
668
|
+
Calls to :meth:`is_present` are cached. You might want to cache the
|
669
|
+
:class:`Executable` object to prevent unnecessary calls to the
|
670
|
+
executable.
|
671
|
+
|
672
|
+
EXAMPLES::
|
673
|
+
|
674
|
+
sage: from sage.features import Executable
|
675
|
+
sage: Executable(name='sh', executable='sh').is_present()
|
676
|
+
FeatureTestResult('sh', True)
|
677
|
+
sage: Executable(name='does-not-exist', executable='does-not-exist-xxxxyxyyxyy').is_present()
|
678
|
+
FeatureTestResult('does-not-exist', False)
|
679
|
+
"""
|
680
|
+
def __init__(self, name, executable, **kwds):
|
681
|
+
r"""
|
682
|
+
TESTS::
|
683
|
+
|
684
|
+
sage: from sage.features import Executable
|
685
|
+
sage: isinstance(Executable(name='sh', executable='sh'), Executable)
|
686
|
+
True
|
687
|
+
"""
|
688
|
+
Feature.__init__(self, name, **kwds)
|
689
|
+
self.executable = executable
|
690
|
+
|
691
|
+
def _is_present(self):
|
692
|
+
r"""
|
693
|
+
Test whether the executable is on the current PATH and functional.
|
694
|
+
|
695
|
+
.. SEEALSO:: :meth:`is_functional`
|
696
|
+
|
697
|
+
EXAMPLES::
|
698
|
+
|
699
|
+
sage: from sage.features import Executable
|
700
|
+
sage: Executable(name='sh', executable='sh').is_present()
|
701
|
+
FeatureTestResult('sh', True)
|
702
|
+
"""
|
703
|
+
result = FileFeature._is_present(self)
|
704
|
+
if not result:
|
705
|
+
return result
|
706
|
+
return self.is_functional()
|
707
|
+
|
708
|
+
def is_functional(self):
|
709
|
+
r"""
|
710
|
+
Return whether an executable in the path is functional.
|
711
|
+
|
712
|
+
This method is used internally and can be overridden in subclasses
|
713
|
+
in order to implement a feature test. It should not be called directly.
|
714
|
+
Use :meth:`Feature.is_present` instead.
|
715
|
+
|
716
|
+
EXAMPLES:
|
717
|
+
|
718
|
+
The function returns ``True`` unless explicitly overwritten::
|
719
|
+
|
720
|
+
sage: from sage.features import Executable
|
721
|
+
sage: Executable(name='sh', executable='sh').is_functional()
|
722
|
+
FeatureTestResult('sh', True)
|
723
|
+
"""
|
724
|
+
return FeatureTestResult(self, True)
|
725
|
+
|
726
|
+
def absolute_filename(self) -> str:
|
727
|
+
r"""
|
728
|
+
The absolute path of the executable as a string.
|
729
|
+
|
730
|
+
EXAMPLES::
|
731
|
+
|
732
|
+
sage: from sage.features import Executable
|
733
|
+
sage: Executable(name='sh', executable='sh').absolute_filename()
|
734
|
+
'/...bin/sh'
|
735
|
+
|
736
|
+
A :exc:`FeatureNotPresentError` is raised if the file cannot be found::
|
737
|
+
|
738
|
+
sage: Executable(name='does-not-exist', executable='does-not-exist-xxxxyxyyxyy').absolute_filename()
|
739
|
+
Traceback (most recent call last):
|
740
|
+
...
|
741
|
+
sage.features.FeatureNotPresentError: does-not-exist is not available.
|
742
|
+
Executable 'does-not-exist-xxxxyxyyxyy' not found on PATH.
|
743
|
+
"""
|
744
|
+
if SAGE_LOCAL:
|
745
|
+
if Path(SAGE_VENV).resolve() != Path(SAGE_LOCAL).resolve():
|
746
|
+
# As sage.env currently gives SAGE_LOCAL a fallback value from SAGE_VENV,
|
747
|
+
# SAGE_LOCAL is never unset. So we only use it if it differs from SAGE_VENV.
|
748
|
+
search_path = ':'.join([os.path.join(SAGE_VENV, 'bin'),
|
749
|
+
os.path.join(SAGE_LOCAL, 'bin')])
|
750
|
+
path = shutil.which(self.executable, path=search_path)
|
751
|
+
if path is not None:
|
752
|
+
return path
|
753
|
+
# Now look up in the regular PATH.
|
754
|
+
path = shutil.which(self.executable)
|
755
|
+
if path is not None:
|
756
|
+
return path
|
757
|
+
raise FeatureNotPresentError(self,
|
758
|
+
reason="Executable {executable!r} not found on PATH.".format(executable=self.executable),
|
759
|
+
resolution=self.resolution())
|
760
|
+
|
761
|
+
|
762
|
+
class StaticFile(FileFeature):
|
763
|
+
r"""
|
764
|
+
A :class:`Feature` which describes the presence of a certain file such as a
|
765
|
+
database.
|
766
|
+
|
767
|
+
EXAMPLES::
|
768
|
+
|
769
|
+
sage: from sage.features import StaticFile
|
770
|
+
sage: StaticFile(name='no_such_file', filename='KaT1aihu', # optional - sage_spkg
|
771
|
+
....: search_path='/', spkg='some_spkg',
|
772
|
+
....: url='http://rand.om').require()
|
773
|
+
Traceback (most recent call last):
|
774
|
+
...
|
775
|
+
FeatureNotPresentError: no_such_file is not available.
|
776
|
+
'KaT1aihu' not found in any of ['/']...
|
777
|
+
To install no_such_file...you can try to run...sage -i some_spkg...
|
778
|
+
Further installation instructions might be available at http://rand.om.
|
779
|
+
"""
|
780
|
+
def __init__(self, name, filename, *, search_path=None, type='optional', **kwds):
|
781
|
+
r"""
|
782
|
+
TESTS::
|
783
|
+
|
784
|
+
sage: from sage.features import StaticFile
|
785
|
+
sage: StaticFile(name='null', filename='null', search_path='/dev')
|
786
|
+
Feature('null')
|
787
|
+
sage: sh = StaticFile(name='shell', filename='sh',
|
788
|
+
....: search_path=("/dev", "/bin", "/usr"))
|
789
|
+
sage: sh
|
790
|
+
Feature('shell')
|
791
|
+
sage: sh.absolute_filename()
|
792
|
+
'/bin/sh'
|
793
|
+
"""
|
794
|
+
Feature.__init__(self, name, type=type, **kwds)
|
795
|
+
self.filename = filename
|
796
|
+
if search_path is None:
|
797
|
+
self.search_path = [SAGE_SHARE]
|
798
|
+
elif isinstance(search_path, str):
|
799
|
+
self.search_path = [search_path]
|
800
|
+
else:
|
801
|
+
self.search_path = list(search_path)
|
802
|
+
|
803
|
+
def absolute_filename(self) -> str:
|
804
|
+
r"""
|
805
|
+
The absolute path of the file as a string.
|
806
|
+
|
807
|
+
EXAMPLES::
|
808
|
+
|
809
|
+
sage: from sage.features import StaticFile
|
810
|
+
sage: from sage.misc.temporary_file import tmp_dir
|
811
|
+
sage: dir_with_file = tmp_dir()
|
812
|
+
sage: file_path = os.path.join(dir_with_file, "file.txt")
|
813
|
+
sage: open(file_path, 'a').close() # make sure the file exists
|
814
|
+
sage: search_path = ( '/foo/bar', dir_with_file ) # file is somewhere in the search path
|
815
|
+
sage: feature = StaticFile(name='file', filename='file.txt', search_path=search_path)
|
816
|
+
sage: feature.absolute_filename() == file_path
|
817
|
+
True
|
818
|
+
|
819
|
+
A :exc:`FeatureNotPresentError` is raised if the file cannot be found::
|
820
|
+
|
821
|
+
sage: from sage.features import StaticFile
|
822
|
+
sage: StaticFile(name='no_such_file', filename='KaT1aihu',\
|
823
|
+
search_path=(), spkg='some_spkg',\
|
824
|
+
url='http://rand.om').absolute_filename() # optional - sage_spkg
|
825
|
+
Traceback (most recent call last):
|
826
|
+
...
|
827
|
+
FeatureNotPresentError: no_such_file is not available.
|
828
|
+
'KaT1aihu' not found in any of []...
|
829
|
+
To install no_such_file...you can try to run...sage -i some_spkg...
|
830
|
+
Further installation instructions might be available at http://rand.om.
|
831
|
+
"""
|
832
|
+
for directory in self.search_path:
|
833
|
+
path = os.path.join(directory, self.filename)
|
834
|
+
if os.path.isfile(path) or os.path.isdir(path):
|
835
|
+
return os.path.abspath(path)
|
836
|
+
reason = "{filename!r} not found in any of {search_path}".format(filename=self.filename, search_path=self.search_path)
|
837
|
+
raise FeatureNotPresentError(self, reason=reason, resolution=self.resolution())
|
838
|
+
|
839
|
+
|
840
|
+
class CythonFeature(Feature):
|
841
|
+
r"""
|
842
|
+
A :class:`Feature` which describes the ability to compile and import
|
843
|
+
a particular piece of Cython code.
|
844
|
+
|
845
|
+
To test the presence of ``name``, the cython compiler is run on
|
846
|
+
``test_code`` and the resulting module is imported.
|
847
|
+
|
848
|
+
EXAMPLES::
|
849
|
+
|
850
|
+
sage: from sage.features import CythonFeature
|
851
|
+
sage: fabs_test_code = '''
|
852
|
+
....: cdef extern from "<math.h>":
|
853
|
+
....: double fabs(double x)
|
854
|
+
....:
|
855
|
+
....: assert fabs(-1) == 1
|
856
|
+
....: '''
|
857
|
+
sage: fabs = CythonFeature("fabs", test_code=fabs_test_code, # needs sage.misc.cython
|
858
|
+
....: spkg='gcc', url='https://gnu.org',
|
859
|
+
....: type='standard')
|
860
|
+
sage: fabs.is_present() # needs sage.misc.cython
|
861
|
+
FeatureTestResult('fabs', True)
|
862
|
+
|
863
|
+
Test various failures::
|
864
|
+
|
865
|
+
sage: broken_code = '''this is not a valid Cython program!'''
|
866
|
+
sage: broken = CythonFeature("broken", test_code=broken_code)
|
867
|
+
sage: broken.is_present()
|
868
|
+
FeatureTestResult('broken', False)
|
869
|
+
|
870
|
+
::
|
871
|
+
|
872
|
+
sage: broken_code = '''cdef extern from "no_such_header_file": pass'''
|
873
|
+
sage: broken = CythonFeature("broken", test_code=broken_code)
|
874
|
+
sage: broken.is_present()
|
875
|
+
FeatureTestResult('broken', False)
|
876
|
+
|
877
|
+
::
|
878
|
+
|
879
|
+
sage: broken_code = '''import no_such_python_module'''
|
880
|
+
sage: broken = CythonFeature("broken", test_code=broken_code)
|
881
|
+
sage: broken.is_present()
|
882
|
+
FeatureTestResult('broken', False)
|
883
|
+
|
884
|
+
::
|
885
|
+
|
886
|
+
sage: broken_code = '''raise AssertionError("sorry!")'''
|
887
|
+
sage: broken = CythonFeature("broken", test_code=broken_code)
|
888
|
+
sage: broken.is_present()
|
889
|
+
FeatureTestResult('broken', False)
|
890
|
+
"""
|
891
|
+
def __init__(self, name, test_code, **kwds):
|
892
|
+
r"""
|
893
|
+
TESTS::
|
894
|
+
|
895
|
+
sage: from sage.features import CythonFeature
|
896
|
+
sage: from sage.features.bliss import BlissLibrary
|
897
|
+
sage: isinstance(BlissLibrary(), CythonFeature) # indirect doctest
|
898
|
+
True
|
899
|
+
"""
|
900
|
+
Feature.__init__(self, name, **kwds)
|
901
|
+
self.test_code = test_code
|
902
|
+
|
903
|
+
def _is_present(self):
|
904
|
+
r"""
|
905
|
+
Run test code to determine whether the shared library is present.
|
906
|
+
|
907
|
+
EXAMPLES::
|
908
|
+
|
909
|
+
sage: from sage.features import CythonFeature
|
910
|
+
sage: empty = CythonFeature("empty", test_code="")
|
911
|
+
sage: empty.is_present() # needs sage.misc.cython
|
912
|
+
FeatureTestResult('empty', True)
|
913
|
+
"""
|
914
|
+
from sage.misc.temporary_file import tmp_filename
|
915
|
+
try:
|
916
|
+
# Available since https://setuptools.pypa.io/en/latest/history.html#v59-0-0
|
917
|
+
from setuptools.errors import CCompilerError
|
918
|
+
except ImportError:
|
919
|
+
try:
|
920
|
+
from distutils.errors import CCompilerError
|
921
|
+
except ImportError:
|
922
|
+
CCompilerError = ()
|
923
|
+
with open(tmp_filename(ext='.pyx'), 'w') as pyx:
|
924
|
+
pyx.write(self.test_code)
|
925
|
+
try:
|
926
|
+
from sage.misc.cython import cython_import
|
927
|
+
except ImportError:
|
928
|
+
return FeatureTestResult(self, False, reason="sage.misc.cython is not available")
|
929
|
+
try:
|
930
|
+
cython_import(pyx.name, verbose=-1)
|
931
|
+
except CCompilerError:
|
932
|
+
return FeatureTestResult(self, False, reason="Failed to compile test code.")
|
933
|
+
except ImportError:
|
934
|
+
return FeatureTestResult(self, False, reason="Failed to import test code.")
|
935
|
+
except Exception:
|
936
|
+
return FeatureTestResult(self, False, reason="Failed to run test code.")
|
937
|
+
return FeatureTestResult(self, True, reason="Test code compiled and imported.")
|
938
|
+
|
939
|
+
|
940
|
+
class PythonModule(Feature):
|
941
|
+
r"""
|
942
|
+
A :class:`Feature` which describes whether a python module can be imported.
|
943
|
+
|
944
|
+
EXAMPLES:
|
945
|
+
|
946
|
+
Not all builds of python include the ``ssl`` module, so you could check
|
947
|
+
whether it is available::
|
948
|
+
|
949
|
+
sage: from sage.features import PythonModule
|
950
|
+
sage: PythonModule("ssl").require() # not tested - output depends on the python build
|
951
|
+
"""
|
952
|
+
def __init__(self, name, **kwds):
|
953
|
+
r"""
|
954
|
+
TESTS::
|
955
|
+
|
956
|
+
sage: from sage.features import PythonModule
|
957
|
+
sage: from sage.features.databases import DatabaseKnotInfo
|
958
|
+
sage: isinstance(DatabaseKnotInfo(), PythonModule) # indirect doctest
|
959
|
+
True
|
960
|
+
"""
|
961
|
+
Feature.__init__(self, name, **kwds)
|
962
|
+
|
963
|
+
def _is_present(self):
|
964
|
+
r"""
|
965
|
+
Return whether the module can be imported. This is determined by
|
966
|
+
actually importing it.
|
967
|
+
|
968
|
+
EXAMPLES::
|
969
|
+
|
970
|
+
sage: from sage.features import PythonModule
|
971
|
+
sage: PythonModule("sys").is_present()
|
972
|
+
FeatureTestResult('sys', True)
|
973
|
+
sage: PythonModule("_no_such_module_").is_present()
|
974
|
+
FeatureTestResult('_no_such_module_', False)
|
975
|
+
"""
|
976
|
+
import importlib
|
977
|
+
try:
|
978
|
+
importlib.import_module(self.name)
|
979
|
+
except ImportError as exception:
|
980
|
+
return FeatureTestResult(self, False, reason=f"Failed to import `{self.name}`: {exception}")
|
981
|
+
return FeatureTestResult(self, True, reason=f"Successfully imported `{self.name}`.")
|