Skip to content

Commit

Permalink
Trac #31292: sage.features.Executable: Add method absolute_filename
Browse files Browse the repository at this point in the history
We refactor `Executable` and `StaticFile` through a base class
`FileFeature`.

The method `absolute_filename` replaces `StaticFile.absolute_path` and
is now also available for `Executable`. (This is for
#33440/#31296/#30818.)

The renaming from `StaticFile.absolute_path` to `absolute_filename`
(with deprecation) is preparation for reclaiming the name
`absolute_path` for a method that returns a `Path` instead of a `str`.

We also add some type annotations.

Follow-up: #31296

URL: https://trac.sagemath.org/31292
Reported by: mkoeppe
Ticket author(s): Matthias Koeppe
Reviewer(s): Tobias Diez
  • Loading branch information
Release Manager committed Mar 8, 2022
2 parents 64f765c + e9568e9 commit 20577cc
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/sage/databases/conway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/sage/databases/cremona.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand Down
2 changes: 1 addition & 1 deletion src/sage/databases/jones.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
156 changes: 132 additions & 24 deletions src/sage/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
As can be seen above, features try to produce helpful error messages.
"""

from __future__ import annotations

import os
import shutil

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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``.
Expand Down Expand Up @@ -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):
Expand All @@ -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.
Expand Down Expand Up @@ -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::
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/sage/features/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,15 @@ 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.
EXAMPLES::
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
Expand Down

0 comments on commit 20577cc

Please sign in to comment.