Skip to content
3 changes: 2 additions & 1 deletion src/sage/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ def resolution(self):
lines.append(ps.spkg_installation_hint(self.spkg, feature=self.name))
if self.url:
lines.append("Further installation instructions might be available at {url}.".format(url=self.url))
self._cache_resolution = "\n".join(lines)
self._cache_resolution = "\n\n".join(lines)
return self._cache_resolution

def joined_features(self):
Expand Down Expand Up @@ -463,6 +463,7 @@ def __str__(self):
lines.append(self.reason)
resolution = self.resolution
if resolution:
lines.append('')
lines.append(str(resolution))
return "\n".join(lines)

Expand Down
8 changes: 4 additions & 4 deletions src/sage/features/databases.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def __init__(self):
sage: isinstance(DatabaseKnotInfo(), DatabaseKnotInfo)
True
"""
PythonModule.__init__(self, 'database_knotinfo', spkg='database_knotinfo')
PythonModule.__init__(self, 'database_knotinfo', spkg='pkg:pypi/database-knotinfo')


class DatabaseMatroids(PythonModule):
Expand All @@ -221,7 +221,7 @@ def __init__(self):
sage: isinstance(DatabaseMatroids(), DatabaseMatroids)
True
"""
PythonModule.__init__(self, 'matroid_database', spkg='matroid_database')
PythonModule.__init__(self, 'matroid_database', spkg='pkg:pypi/matroid-database')


class DatabaseCubicHecke(PythonModule):
Expand All @@ -247,7 +247,7 @@ def __init__(self):
sage: isinstance(DatabaseCubicHecke(), DatabaseCubicHecke)
True
"""
PythonModule.__init__(self, 'database_cubic_hecke', spkg='database_cubic_hecke')
PythonModule.__init__(self, 'database_cubic_hecke', spkg='pkg:pypi/database-cubic-hecke')


class DatabaseReflexivePolytopes(StaticFile):
Expand Down Expand Up @@ -289,7 +289,7 @@ def __init__(self, name='polytopes_db'):


def all_features():
return [PythonModule('conway_polynomials', spkg='conway_polynomials', type='standard'),
return [PythonModule('conway_polynomials', spkg='pkg:pypi/conway-polynomials', type='standard'),
DatabaseCremona(),
DatabaseCremona('cremona_mini', type='standard'),
DatabaseEllcurves(),
Expand Down
2 changes: 1 addition & 1 deletion src/sage/features/igraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self):
True
"""
JoinFeature.__init__(self, 'python_igraph',
[PythonModule('igraph', spkg='python_igraph',
[PythonModule('igraph', spkg='pkg:pypi/igraph',
url='http://igraph.org')])

def all_features():
Expand Down
8 changes: 4 additions & 4 deletions src/sage/features/mip_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def __init__(self):
FeatureTestResult('cplex', True)
"""
MIPBackend.__init__(self, 'cplex',
spkg='sage_numerical_backends_cplex')
spkg='pkg:pypi/sage-numerical-backends-cplex')


class Gurobi(MIPBackend):
Expand All @@ -68,7 +68,7 @@ def __init__(self):
FeatureTestResult('gurobi', True)
"""
MIPBackend.__init__(self, 'gurobi',
spkg='sage_numerical_backends_gurobi')
spkg='pkg:pypi/sage-numerical-backends-gurobi')


class COIN(JoinFeature):
Expand All @@ -85,7 +85,7 @@ def __init__(self):
"""
JoinFeature.__init__(self, 'sage_numerical_backends_coin',
[MIPBackend('coin')],
spkg='sage_numerical_backends_coin')
spkg='pkg:pypi/sage-numerical-backends-coin')


class CVXOPT(JoinFeature):
Expand All @@ -103,7 +103,7 @@ def __init__(self):
JoinFeature.__init__(self, 'cvxopt',
[MIPBackend('CVXOPT'),
PythonModule('cvxopt')],
spkg='cvxopt',
spkg='pkg:pypi/cvxopt',
type='standard')


Expand Down
2 changes: 1 addition & 1 deletion src/sage/features/normaliz.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __init__(self):
True
"""
JoinFeature.__init__(self, 'pynormaliz',
[PythonModule('PyNormaliz', spkg='pynormaliz')])
[PythonModule('PyNormaliz', spkg='pkg:pypi/pynormaliz')])


def all_features():
Expand Down
2 changes: 1 addition & 1 deletion src/sage/features/phitigra.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self):
sage: isinstance(Phitigra(), Phitigra)
True
"""
PythonModule.__init__(self, 'phitigra', spkg='phitigra')
PythonModule.__init__(self, 'phitigra', spkg='pkg:pypi/phitigra')


def all_features():
Expand Down
132 changes: 125 additions & 7 deletions src/sage/features/pkg_systems.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@
"""

# *****************************************************************************
# Copyright (C) 2021-2022 Matthias Koeppe
# Copyright (C) 2021-2024 Matthias Koeppe
#
# Distributed under the terms of the GNU General Public License (GPL)
# as published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
# https://www.gnu.org/licenses/
# *****************************************************************************

import os
import re
import shlex
import sys
import sysconfig

from . import Feature


Expand Down Expand Up @@ -56,6 +62,29 @@
feature = spkgs
return self._spkg_installation_hint(spkgs, prompt, feature)

def _system_packages(self, spkgs):
r"""
Return system packages corresponding to SPKG names or PURLs.

INPUT:

- ``spkgs`` -- string, whitespace-separated list of SPKG names or PURLs

OUTPUT: list of strings.

EXAMPLES::

sage: from sage.features.pkg_systems import PackageSystem
sage: debian = PackageSystem('debian')
sage: debian._system_packages('fflas_ffpack pypi/cvxopt pkg:generic/gmp') # optional - SAGE_ROOT
['fflas-ffpack', 'libgmp-dev']
"""
from subprocess import run, CalledProcessError
system = self.name
proc = run(f'sage-get-system-packages {system} {spkgs}',
shell=True, capture_output=True, text=True, check=True)
return proc.stdout.strip().split('\n')

def _spkg_installation_hint(self, spkgs, prompt, feature):
r"""
Return a string that explains how to install ``feature``.
Expand All @@ -69,18 +98,17 @@
sage: fedora.spkg_installation_hint('openblas') # optional - SAGE_ROOT
'To install openblas using the fedora package manager, you can try to run:\n!sudo yum install openblas-devel'
"""
from subprocess import run, CalledProcessError, PIPE
from subprocess import run, CalledProcessError
lines = []
system = self.name
try:
proc = run(f'sage-get-system-packages {system} {spkgs}',
shell=True, capture_output=True, text=True, check=True)
system_packages = proc.stdout.strip()
system_packages = ' '.join(shlex.quote(pkg)
for pkg in self._system_packages(spkgs))
print_sys = f'sage-print-system-package-command {system} --verbose --sudo --prompt="{prompt}"'
command = f'{print_sys} update && {print_sys} install {system_packages}'
proc = run(command, shell=True, capture_output=True, text=True, check=True)
command = proc.stdout.strip()
if command:
command = proc.stdout
if command.strip():
lines.append(f'To install {feature} using the {system} package manager, you can try to run:')
lines.append(command)
return '\n'.join(lines)
Expand Down Expand Up @@ -148,11 +176,37 @@
To install foobarability using the Sage package manager, you can try to run:
### sage -i foo bar
"""
spkgs = ' '.join(shlex.quote(pkg) for pkg in self._system_packages(spkgs))
lines = []
lines.append(f'To install {feature} using the Sage package manager, you can try to run:')
lines.append(f'{prompt}sage -i {spkgs}')
return '\n'.join(lines)

def _system_packages(self, spkgs):
r"""
Return system packages corresponding to SPKG names or PURLs.

INPUT:

- ``spkgs`` -- string, whitespace-separated list of SPKG names or PURLs

OUTPUT: list of strings.

EXAMPLES::

sage: from sage.features.pkg_systems import SagePackageSystem
sage: SagePackageSystem()._system_packages('gfortran')
['gfortran']
sage: SagePackageSystem()._system_packages('pkg:pypi/cvxopt') # needs sage_spkg
['cvxopt']
"""
if 'pkg:' not in spkgs and 'pypi/' not in spkgs and 'generic/' not in spkgs:
return spkgs.split()
from subprocess import run, CalledProcessError
proc = run(f'sage-package list {spkgs}',
shell=True, capture_output=True, text=True, check=True)
return proc.stdout.strip().split('\n')


class PipPackageSystem(PackageSystem):
r"""
Expand Down Expand Up @@ -193,3 +247,67 @@
return True
except CalledProcessError:
return False

def _spkg_installation_hint(self, spkgs, prompt, feature):
r"""
Return a string that explains how to install ``feature``.

EXAMPLES::

sage: from sage.features.pkg_systems import PipPackageSystem
sage: print(PipPackageSystem().spkg_installation_hint(['admcycles'], feature='admcycles')) # indirect doctest
To install admcycles...pip install admcycles...
"""
lines = []
# https://github.com/pypa/pip/blob/51de88ca6459fdd5213f86a54b021a80884572f9/src/pip/_internal/utils/virtualenv.py#L14
is_virtualenv = sys.prefix != getattr(sys, "base_prefix", sys.prefix)
# https://github.com/pypa/pip/blob/51de88ca6459fdd5213f86a54b021a80884572f9/src/pip/_internal/utils/misc.py#L648
marker = os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED")
is_externally_managed = os.path.isfile(marker)
pip_packages = ' '.join(shlex.quote(pkg) for pkg in self._system_packages(spkgs))
if not is_virtualenv and is_externally_managed:
lines.append(f'To install {feature} using the pip package manager:')
lines.append(f'Note that this Sage is installed in an externally managed Python environment.')
lines.append(f'It is recommended to first create a virtual environment:')
lines.append(f'{prompt}sage -python -m venv --system-site-packages ~/.sage/venv')
lines.append(f'Then quit the current Sage:')
lines.append(f' exit')
lines.append(f'Next, in the shell, activate the new virtual environment:')
lines.append(f' $ . ~/.sage/venv/bin/activate')
lines.append(f' $ pip install {pip_packages}')
lines.append(f'Finally, start Sage from the new virtual environment:')
lines.append(f' $ sage')
lines.append(f'To exit the virtual environment after use:')
lines.append(f' $ deactivate')
return '\n'.join(lines)

Check warning on line 282 in src/sage/features/pkg_systems.py

View check run for this annotation

Codecov / codecov/patch

src/sage/features/pkg_systems.py#L269-L282

Added lines #L269 - L282 were not covered by tests
return super()._spkg_installation_hint(spkgs, prompt, feature)

def _system_packages(self, spkgs):
r"""
Return system packages corresponding to SPKG names or PURLs.

INPUT:

- ``spkgs`` -- string, whitespace-separated list of SPKG names or PURLs

OUTPUT: list of strings.

EXAMPLES::

sage: from sage.features.pkg_systems import PipPackageSystem
sage: PipPackageSystem()._system_packages('pypi/cvxopt pkg:pypi/pynormaliz')
['cvxopt...', 'pynormaliz...']
sage: PipPackageSystem()._system_packages('pypi/cvxopt pkg:pypi/pynormaliz dateutil') # optional - sage_spkg
['cvxopt...', 'python-dateutil...', 'pynormaliz...']
"""
result = super()._system_packages(spkgs)
if result:
return result
all_packages = spkgs.split()
pypi_packages = [m.group(2) for package in all_packages

Check warning on line 307 in src/sage/features/pkg_systems.py

View check run for this annotation

Codecov / codecov/patch

src/sage/features/pkg_systems.py#L306-L307

Added lines #L306 - L307 were not covered by tests
if (m := re.fullmatch('(pkg:)?pypi/(.*)', package))]
other_packages = [package for package in all_packages

Check warning on line 309 in src/sage/features/pkg_systems.py

View check run for this annotation

Codecov / codecov/patch

src/sage/features/pkg_systems.py#L309

Added line #L309 was not covered by tests
if not re.fullmatch('(pkg:)?pypi/(.*)', package)]
if other_packages:
return []
return sorted(pypi_packages)

Check warning on line 313 in src/sage/features/pkg_systems.py

View check run for this annotation

Codecov / codecov/patch

src/sage/features/pkg_systems.py#L311-L313

Added lines #L311 - L313 were not covered by tests
2 changes: 1 addition & 1 deletion src/sage/features/polymake.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __init__(self):
True
"""
JoinFeature.__init__(self, "jupymake",
[PythonModule("JuPyMake", spkg='jupymake')])
[PythonModule('JuPyMake', spkg='pkg:pypi/jupymake')])


def all_features():
Expand Down
2 changes: 1 addition & 1 deletion src/sage/features/sat.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(self):
True
"""
PythonModule.__init__(self, "pycosat",
spkg="pycosat", type="optional")
spkg="pkg:pypi/pycosat", type="optional")


class Pycryptosat(PythonModule):
Expand Down
2 changes: 1 addition & 1 deletion src/sage/features/sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __init__(self):
sage: isinstance(Sphinx(), Sphinx)
True
"""
PythonModule.__init__(self, 'sphinx', spkg='sphinx', type='standard')
PythonModule.__init__(self, 'sphinx', spkg='pkg:pypi/sphinx', type='standard')


def all_features():
Expand Down
34 changes: 17 additions & 17 deletions src/sage/features/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@


def all_features():
return [PythonModule('cvxopt', spkg='cvxopt', type='standard'),
PythonModule('fpylll', spkg='fpylll', type='standard'),
JoinFeature('ipython', (PythonModule('IPython'),), spkg='ipython', type='standard'),
JoinFeature('lrcalc_python', (PythonModule('lrcalc'),), spkg='lrcalc_python', type='standard'),
PythonModule('mpmath', spkg='mpmath', type='standard'),
PythonModule('networkx', spkg='networkx', type='standard'),
PythonModule('numpy', spkg='numpy', type='standard'),
PythonModule('pexpect', spkg='pexpect', type='standard'),
JoinFeature('pillow', (PythonModule('PIL'),), spkg='pillow', type='standard'),
JoinFeature('pplpy', (PythonModule('ppl'),), spkg='pplpy', type='standard'),
PythonModule('primecountpy', spkg='primecountpy', type='standard'),
PythonModule('ptyprocess', spkg='ptyprocess', type='standard'),
PythonModule('pyparsing', spkg='pyparsing', type='standard'),
PythonModule('requests', spkg='requests', type='standard'),
PythonModule('rpy2', spkg='rpy2', type='standard'),
PythonModule('scipy', spkg='scipy', type='standard'),
PythonModule('sympy', spkg='sympy', type='standard')]
return [PythonModule('cvxopt', spkg='pkg:pypi/cvxopt', type='standard'),
PythonModule('fpylll', spkg='pkg:pypi/fpylll', type='standard'),
JoinFeature('ipython', (PythonModule('IPython'),), spkg='pkg:pypi/ipython', type='standard'),
JoinFeature('lrcalc_python', (PythonModule('lrcalc'),), spkg='pkg:pypi/lrcalc', type='standard'),
PythonModule('mpmath', spkg='pkg:pypi/mpmath', type='standard'),
PythonModule('networkx', spkg='pkg:pypi/networkx', type='standard'),
PythonModule('numpy', spkg='pkg:pypi/numpy', type='standard'),
PythonModule('pexpect', spkg='pkg:pypi/pexpect', type='standard'),
JoinFeature('pillow', (PythonModule('PIL'),), spkg='pkg:pypi/pillow', type='standard'),
JoinFeature('pplpy', (PythonModule('ppl'),), spkg='pkg:pypi/pplpy', type='standard'),
PythonModule('primecountpy', spkg='pkg:pypi/primecountpy', type='standard'),
PythonModule('ptyprocess', spkg='pkg:pypi/ptyprocess', type='standard'),
PythonModule('pyparsing', spkg='pkg:pypi/pyparsing', type='standard'),
PythonModule('requests', spkg='pkg:pypi/requests', type='standard'),
PythonModule('rpy2', spkg='pkg:pypi/rpy2', type='standard'),
PythonModule('scipy', spkg='pkg:pypi/scipy', type='standard'),
PythonModule('sympy', spkg='pkg:pypi/sympy', type='standard')]
2 changes: 1 addition & 1 deletion src/sage/features/symengine_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self):
True
"""
JoinFeature.__init__(self, 'symengine_py',
[PythonModule('symengine', spkg='symengine_py',
[PythonModule('symengine', spkg='pkg:pypi/symengine.py',
url='https://pypi.org/project/symengine')])

def all_features():
Expand Down
4 changes: 2 additions & 2 deletions src/sage/geometry/cone.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@
from sage.features import PythonModule
lazy_import('ppl', ['C_Polyhedron', 'Generator_System', 'Constraint_System',
'Linear_Expression', 'Poly_Con_Relation'],
feature=PythonModule("ppl", spkg='pplpy', type='standard'))
feature=PythonModule('ppl', spkg='pkg:pypi/pplpy', type='standard'))
lazy_import('ppl', ['ray', 'point'], as_=['PPL_ray', 'PPL_point'],
feature=PythonModule("ppl", spkg='pplpy', type='standard'))
feature=PythonModule('ppl', spkg='pkg:pypi/pplpy', type='standard'))


def is_Cone(x):
Expand Down
4 changes: 2 additions & 2 deletions src/sage/geometry/lattice_polytope.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@
from sage.features import PythonModule
from sage.features.palp import PalpExecutable
lazy_import('ppl', ['C_Polyhedron', 'Generator_System', 'Linear_Expression'],
feature=PythonModule("ppl", spkg='pplpy', type='standard'))
feature=PythonModule('ppl', spkg='pkg:pypi/pplpy', type='standard'))
lazy_import('ppl', 'point', as_='PPL_point',
feature=PythonModule("ppl", spkg='pplpy', type='standard'))
feature=PythonModule('ppl', spkg='pkg:pypi/pplpy', type='standard'))

from sage.matrix.constructor import matrix
from sage.structure.element import Matrix
Expand Down
Loading
Loading