diff --git a/src/sage/features/__init__.py b/src/sage/features/__init__.py index 81602180d80..d9207e2e1a2 100644 --- a/src/sage/features/__init__.py +++ b/src/sage/features/__init__.py @@ -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): @@ -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) diff --git a/src/sage/features/databases.py b/src/sage/features/databases.py index 9d070d932ed..b0c567dc3d1 100644 --- a/src/sage/features/databases.py +++ b/src/sage/features/databases.py @@ -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): @@ -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): @@ -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): @@ -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(), diff --git a/src/sage/features/igraph.py b/src/sage/features/igraph.py index 9bb61c28454..e2d8d94b062 100644 --- a/src/sage/features/igraph.py +++ b/src/sage/features/igraph.py @@ -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(): diff --git a/src/sage/features/mip_backends.py b/src/sage/features/mip_backends.py index a6aa0ff2525..9a1c344a521 100644 --- a/src/sage/features/mip_backends.py +++ b/src/sage/features/mip_backends.py @@ -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): @@ -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): @@ -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): @@ -103,7 +103,7 @@ def __init__(self): JoinFeature.__init__(self, 'cvxopt', [MIPBackend('CVXOPT'), PythonModule('cvxopt')], - spkg='cvxopt', + spkg='pkg:pypi/cvxopt', type='standard') diff --git a/src/sage/features/normaliz.py b/src/sage/features/normaliz.py index 5b872739bc0..c11e2168451 100644 --- a/src/sage/features/normaliz.py +++ b/src/sage/features/normaliz.py @@ -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(): diff --git a/src/sage/features/phitigra.py b/src/sage/features/phitigra.py index ea4d855cf53..b4516cbea1d 100644 --- a/src/sage/features/phitigra.py +++ b/src/sage/features/phitigra.py @@ -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(): diff --git a/src/sage/features/pkg_systems.py b/src/sage/features/pkg_systems.py index 4f6db21b735..eb852d46765 100644 --- a/src/sage/features/pkg_systems.py +++ b/src/sage/features/pkg_systems.py @@ -4,7 +4,7 @@ """ # ***************************************************************************** -# 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 @@ -12,6 +12,12 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** +import os +import re +import shlex +import sys +import sysconfig + from . import Feature @@ -56,6 +62,29 @@ def spkg_installation_hint(self, spkgs, *, prompt=" !", feature=None): 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``. @@ -69,18 +98,17 @@ def _spkg_installation_hint(self, spkgs, prompt, feature): 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) @@ -148,11 +176,37 @@ def _spkg_installation_hint(self, spkgs, prompt, feature): 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""" @@ -193,3 +247,67 @@ def _is_present(self): 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) + 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 + if (m := re.fullmatch('(pkg:)?pypi/(.*)', package))] + other_packages = [package for package in all_packages + if not re.fullmatch('(pkg:)?pypi/(.*)', package)] + if other_packages: + return [] + return sorted(pypi_packages) diff --git a/src/sage/features/polymake.py b/src/sage/features/polymake.py index 6428d68a29d..37cda29e342 100644 --- a/src/sage/features/polymake.py +++ b/src/sage/features/polymake.py @@ -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(): diff --git a/src/sage/features/sat.py b/src/sage/features/sat.py index 6a05491ddb7..1f048c9d11c 100644 --- a/src/sage/features/sat.py +++ b/src/sage/features/sat.py @@ -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): diff --git a/src/sage/features/sphinx.py b/src/sage/features/sphinx.py index ec7c8be17b6..b1c3e24e252 100644 --- a/src/sage/features/sphinx.py +++ b/src/sage/features/sphinx.py @@ -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(): diff --git a/src/sage/features/standard.py b/src/sage/features/standard.py index c4055263e79..184bc58ba9a 100644 --- a/src/sage/features/standard.py +++ b/src/sage/features/standard.py @@ -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')] diff --git a/src/sage/features/symengine_py.py b/src/sage/features/symengine_py.py index 96a64ca60b3..78c19ef62e0 100644 --- a/src/sage/features/symengine_py.py +++ b/src/sage/features/symengine_py.py @@ -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(): diff --git a/src/sage/geometry/cone.py b/src/sage/geometry/cone.py index 17f58378373..727ba686317 100644 --- a/src/sage/geometry/cone.py +++ b/src/sage/geometry/cone.py @@ -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): diff --git a/src/sage/geometry/lattice_polytope.py b/src/sage/geometry/lattice_polytope.py index 67f6e74ea89..697dc8607f9 100644 --- a/src/sage/geometry/lattice_polytope.py +++ b/src/sage/geometry/lattice_polytope.py @@ -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 diff --git a/src/sage/geometry/polyhedron/backend_ppl.py b/src/sage/geometry/polyhedron/backend_ppl.py index c930039886b..cf16c9c9463 100644 --- a/src/sage/geometry/polyhedron/backend_ppl.py +++ b/src/sage/geometry/polyhedron/backend_ppl.py @@ -16,7 +16,7 @@ from sage.features import PythonModule lazy_import('ppl', ['C_Polyhedron', 'Generator_System', 'Constraint_System', 'Linear_Expression', 'line', 'ray', 'point'], - feature=PythonModule("ppl", spkg='pplpy', type='standard')) + feature=PythonModule('ppl', spkg='pkg:pypi/pplpy', type='standard')) ######################################################################### diff --git a/src/sage/misc/lazy_import.pyx b/src/sage/misc/lazy_import.pyx index cde9be93d7e..d8096c1f907 100644 --- a/src/sage/misc/lazy_import.pyx +++ b/src/sage/misc/lazy_import.pyx @@ -1091,11 +1091,11 @@ def lazy_import(module, names, as_=None, *, sage: from sage.features import PythonModule sage: lazy_import('ppl', 'equation', - ....: feature=PythonModule('ppl', spkg='pplpy', type='standard')) + ....: feature=PythonModule('ppl', spkg='pkg:pypi/pplpy', type='standard')) sage: equation # needs pplpy sage: lazy_import('PyNormaliz', 'NmzListConeProperties', - ....: feature=PythonModule('PyNormaliz', spkg='pynormaliz')) + ....: feature=PythonModule('PyNormaliz', spkg='pkg:pypi/pynormaliz')) sage: NmzListConeProperties # optional - pynormaliz sage: lazy_import('foo', 'not_there',