diff --git a/src/sage/databases/conway.py b/src/sage/databases/conway.py index 2119549c13d..6ef5c61bf47 100644 --- a/src/sage/databases/conway.py +++ b/src/sage/databases/conway.py @@ -101,7 +101,7 @@ def __init__(self): """ global _conwaydict if _conwaydict is None: - _CONWAYDATA = DatabaseConwayPolynomials().absolute_path() + _CONWAYDATA = DatabaseConwayPolynomials().absolute_filename() with open(_CONWAYDATA, 'rb') as f: _conwaydict = pickle.load(f) self._store = _conwaydict diff --git a/src/sage/databases/cremona.py b/src/sage/databases/cremona.py index 6353ad7bbfe..f6364976dc7 100644 --- a/src/sage/databases/cremona.py +++ b/src/sage/databases/cremona.py @@ -669,7 +669,7 @@ def __init__(self, name, read_only=True, build=False): """ self.name = name name = name.replace(' ', '_') - db_path = DatabaseCremona(name=name).absolute_path() + db_path = DatabaseCremona(name=name).absolute_filename() if build: if read_only: raise RuntimeError('The database must not be read_only.') diff --git a/src/sage/databases/jones.py b/src/sage/databases/jones.py index d6eef45cafd..da1e730c65c 100644 --- a/src/sage/databases/jones.py +++ b/src/sage/databases/jones.py @@ -225,7 +225,7 @@ def get(self, S, var='a'): ValueError: S must be a list of primes """ if self.root is None: - self.root = load(DatabaseJones().absolute_path()) + self.root = load(DatabaseJones().absolute_filename()) try: S = list(S) except TypeError: diff --git a/src/sage/features/__init__.py b/src/sage/features/__init__.py index 4bdc6e4c99b..f75015d7398 100644 --- a/src/sage/features/__init__.py +++ b/src/sage/features/__init__.py @@ -52,6 +52,8 @@ As can be seen above, features try to produce helpful error messages. """ +from __future__ import annotations + import os import shutil @@ -385,7 +387,7 @@ def __repr__(self): def package_systems(): """ - Return a list of :class:~sage.features.pkg_systems.PackageSystem` objects + Return a list of :class:`~sage.features.pkg_systems.PackageSystem` objects representing the available package systems. The list is ordered by decreasing preference. @@ -417,7 +419,101 @@ def package_systems(): return _cache_package_systems -class Executable(Feature): +class FileFeature(Feature): + r""" + Base class for features that describe a file or directory in the file system. + + A subclass should implement a method :meth:`absolute_filename`. + + EXAMPLES: + + Two direct concrete subclasses of :class:`FileFeature` are defined:: + + sage: from sage.features import StaticFile, Executable, FileFeature + sage: issubclass(StaticFile, FileFeature) + True + sage: issubclass(Executable, FileFeature) + True + + To work with the file described by the feature, use the method :meth:`absolute_filename`. + A :class:`FeatureNotPresentError` is raised if the file cannot be found:: + + sage: Executable(name="does-not-exist", executable="does-not-exist-xxxxyxyyxyy").absolute_path() + Traceback (most recent call last): + ... + sage.features.FeatureNotPresentError: does-not-exist is not available. + Executable 'does-not-exist-xxxxyxyyxyy' not found on PATH. + + A :class:`FileFeature` also provides the :meth:`is_present` method to test for + the presence of the file at run time. This is inherited from the base class + :class:`Feature`:: + + sage: Executable(name="sh", executable="sh").is_present() + FeatureTestResult('sh', True) + """ + def _is_present(self): + r""" + Whether the file is present. + + EXAMPLES:: + + sage: from sage.features import StaticFile + sage: StaticFile(name="no_such_file", filename="KaT1aihu", spkg="some_spkg", url="http://rand.om").is_present() + FeatureTestResult('no_such_file', False) + """ + try: + abspath = self.absolute_filename() + return FeatureTestResult(self, True, reason="Found at `{abspath}`.".format(abspath=abspath)) + except FeatureNotPresentError as e: + return FeatureTestResult(self, False, reason=e.reason, resolution=e.resolution) + + def absolute_filename(self) -> str: + r""" + The absolute path of the file as a string. + + Concrete subclasses must override this abstract method. + + TESTS:: + + sage: from sage.features import FileFeature + sage: FileFeature(name="abstract_file").absolute_filename() + Traceback (most recent call last): + ... + NotImplementedError + """ + # We do not use sage.misc.abstract_method here because that is provided by + # the distribution sagemath-objects, which is not an install-requires of + # the distribution sagemath-environment. + raise NotImplementedError + + def absolute_path(self): + r""" + Deprecated alias for :meth:`absolute_filename`. + + Deprecated to make way for a method of this name returning a ``Path``. + + EXAMPLES:: + + sage: from sage.features import Executable + sage: Executable(name="sh", executable="sh").absolute_path() + doctest:warning... + DeprecationWarning: method absolute_path has been replaced by absolute_filename + See https://trac.sagemath.org/31292 for details. + '/...bin/sh' + """ + try: + from sage.misc.superseded import deprecation + except ImportError: + # The import can fail because sage.misc.superseded is provided by + # the distribution sagemath-objects, which is not an + # install-requires of the distribution sagemath-environment. + pass + else: + deprecation(31292, 'method absolute_path has been replaced by absolute_filename') + return self.absolute_filename() + + +class Executable(FileFeature): r""" A feature describing an executable in the ``PATH``. @@ -461,8 +557,9 @@ def _is_present(self): sage: Executable(name="sh", executable="sh").is_present() FeatureTestResult('sh', True) """ - if shutil.which(self.executable) is None: - return FeatureTestResult(self, False, "Executable {executable!r} not found on PATH.".format(executable=self.executable)) + result = FileFeature._is_present(self) + if not result: + return result return self.is_functional() def is_functional(self): @@ -479,8 +576,33 @@ def is_functional(self): """ return FeatureTestResult(self, True) + def absolute_filename(self) -> str: + r""" + The absolute path of the executable as a string. + + EXAMPLES:: + + sage: from sage.features import Executable + sage: Executable(name="sh", executable="sh").absolute_filename() + '/...bin/sh' + + A :class:`FeatureNotPresentError` is raised if the file cannot be found:: + + sage: Executable(name="does-not-exist", executable="does-not-exist-xxxxyxyyxyy").absolute_path() + Traceback (most recent call last): + ... + sage.features.FeatureNotPresentError: does-not-exist is not available. + Executable 'does-not-exist-xxxxyxyyxyy' not found on PATH. + """ + path = shutil.which(self.executable) + if path is not None: + return path + raise FeatureNotPresentError(self, + reason="Executable {executable!r} not found on PATH.".format(executable=self.executable), + resolution=self.resolution()) + -class StaticFile(Feature): +class StaticFile(FileFeature): r""" A :class:`Feature` which describes the presence of a certain file such as a database. @@ -511,23 +633,9 @@ def __init__(self, name, filename, search_path=None, **kwds): else: self.search_path = list(search_path) - def _is_present(self): - r""" - Whether the static file is present. - - sage: from sage.features import StaticFile - sage: StaticFile(name="no_such_file", filename="KaT1aihu", spkg="some_spkg", url="http://rand.om").is_present() - FeatureTestResult('no_such_file', False) - """ - try: - abspath = self.absolute_path() - return FeatureTestResult(self, True, reason="Found at `{abspath}`.".format(abspath=abspath)) - except FeatureNotPresentError as e: - return FeatureTestResult(self, False, reason=e.reason, resolution=e.resolution) - - def absolute_path(self): + def absolute_filename(self) -> str: r""" - The absolute path of the file. + The absolute path of the file as a string. EXAMPLES:: @@ -538,13 +646,13 @@ def absolute_path(self): sage: open(file_path, 'a').close() # make sure the file exists sage: search_path = ( '/foo/bar', dir_with_file ) # file is somewhere in the search path sage: feature = StaticFile(name="file", filename="file.txt", search_path=search_path) - sage: feature.absolute_path() == file_path + sage: feature.absolute_filename() == file_path True - A ``FeatureNotPresentError`` is raised if the file cannot be found:: + 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_path() # 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. diff --git a/src/sage/features/latex.py b/src/sage/features/latex.py index eba827a4978..b8915502582 100644 --- a/src/sage/features/latex.py +++ b/src/sage/features/latex.py @@ -153,7 +153,7 @@ class TeXFile(StaticFile): def __init__(self, name, filename, **kwds): StaticFile.__init__(self, name, filename, search_path=[], **kwds) - def absolute_path(self): + def absolute_filename(self) -> str: r""" The absolute path of the file. @@ -161,7 +161,7 @@ def absolute_path(self): sage: from sage.features.latex import TeXFile sage: feature = TeXFile('latex_class_article', 'article.cls') - sage: feature.absolute_path() # optional - pdflatex + sage: feature.absolute_filename() # optional - pdflatex '.../latex/base/article.cls' """ from subprocess import run, CalledProcessError, PIPE