diff --git a/lib/spack/docs/conf.py b/lib/spack/docs/conf.py index e3f0df9a4ba..277ece75f5b 100644 --- a/lib/spack/docs/conf.py +++ b/lib/spack/docs/conf.py @@ -225,8 +225,10 @@ def setup(sphinx): ("py:class", "llnl.util.lang.T"), ("py:class", "llnl.util.lang.KT"), ("py:class", "llnl.util.lang.VT"), + ("py:class", "llnl.util.lang.ClassPropertyType"), ("py:obj", "llnl.util.lang.KT"), ("py:obj", "llnl.util.lang.VT"), + ("py:obj", "llnl.util.lang.ClassPropertyType"), ] # The reST default role (used for this markup: `text`) to use for all documents. diff --git a/lib/spack/llnl/util/lang.py b/lib/spack/llnl/util/lang.py index 05100e087be..ac44fe74937 100644 --- a/lib/spack/llnl/util/lang.py +++ b/lib/spack/llnl/util/lang.py @@ -15,7 +15,19 @@ import typing import warnings from datetime import datetime, timedelta -from typing import Callable, Dict, Iterable, List, Mapping, Optional, Tuple, TypeVar +from typing import ( + Any, + Callable, + Dict, + Generic, + Iterable, + List, + Mapping, + Optional, + Tuple, + TypeVar, + Union, +) # Ignore emacs backups when listing modules ignore_modules = r"^\.#|~$" @@ -1047,19 +1059,28 @@ def __exit__(self, exc_type, exc_value, tb): return True -class classproperty: +ClassPropertyType = TypeVar("ClassPropertyType") + + +class classproperty(Generic[ClassPropertyType]): """Non-data descriptor to evaluate a class-level property. The function that performs - the evaluation is injected at creation time and take an instance (could be None) and - an owner (i.e. the class that originated the instance) + the evaluation is injected at creation time and takes an owner (i.e., the class that + originated the instance). """ - def __init__(self, callback): + def __init__(self, callback: Callable[[Any], ClassPropertyType]) -> None: self.callback = callback - def __get__(self, instance, owner): + def __get__(self, instance, owner) -> ClassPropertyType: return self.callback(owner) +#: A type alias that represents either a classproperty descriptor or a constant value of the same +#: type. This allows derived classes to override a computed class-level property with a constant +#: value while retaining type compatibility. +ClassProperty = Union[ClassPropertyType, classproperty[ClassPropertyType]] + + class DeprecatedProperty: """Data descriptor to error or warn when a deprecated property is accessed. diff --git a/lib/spack/spack/build_systems/python.py b/lib/spack/spack/build_systems/python.py index eff00b6e8a6..a88927a8a72 100644 --- a/lib/spack/spack/build_systems/python.py +++ b/lib/spack/spack/build_systems/python.py @@ -13,9 +13,9 @@ import archspec import llnl.util.filesystem as fs -import llnl.util.lang as lang import llnl.util.tty as tty from llnl.util.filesystem import HeaderList, LibraryList, join_path +from llnl.util.lang import ClassProperty, classproperty, match_predicate import spack.builder import spack.config @@ -139,7 +139,7 @@ def view_file_conflicts(self, view, merge_map): ext_map = view.extensions_layout.extension_map(self.extendee_spec) namespaces = set(x.package.py_namespace for x in ext_map.values()) namespace_re = r"site-packages/{0}/__init__.py".format(self.py_namespace) - find_namespace = lang.match_predicate(namespace_re) + find_namespace = match_predicate(namespace_re) if self.py_namespace in namespaces: conflicts = list(x for x in conflicts if not find_namespace(x)) @@ -206,7 +206,7 @@ def remove_files_from_view(self, view, merge_map): spec.package.py_namespace for name, spec in ext_map.items() if name != self.name ) if self.py_namespace in remaining_namespaces: - namespace_init = lang.match_predicate( + namespace_init = match_predicate( r"site-packages/{0}/__init__.py".format(self.py_namespace) ) ignore_namespace = True @@ -324,6 +324,27 @@ def get_external_python_for_prefix(self): raise StopIteration("No external python could be detected for %s to depend on" % self.spec) +def _homepage(cls: "PythonPackage") -> Optional[str]: + """Get the homepage from PyPI if available.""" + if cls.pypi: + name = cls.pypi.split("/")[0] + return f"https://pypi.org/project/{name}/" + return None + + +def _url(cls: "PythonPackage") -> Optional[str]: + if cls.pypi: + return f"https://files.pythonhosted.org/packages/source/{cls.pypi[0]}/{cls.pypi}" + return None + + +def _list_url(cls: "PythonPackage") -> Optional[str]: + if cls.pypi: + name = cls.pypi.split("/")[0] + return f"https://pypi.org/simple/{name}/" + return None + + class PythonPackage(PythonExtension): """Specialized class for packages that are built using pip.""" @@ -351,25 +372,9 @@ class PythonPackage(PythonExtension): py_namespace: Optional[str] = None - @lang.classproperty - def homepage(cls) -> Optional[str]: # type: ignore[override] - if cls.pypi: - name = cls.pypi.split("/")[0] - return f"https://pypi.org/project/{name}/" - return None - - @lang.classproperty - def url(cls) -> Optional[str]: - if cls.pypi: - return f"https://files.pythonhosted.org/packages/source/{cls.pypi[0]}/{cls.pypi}" - return None - - @lang.classproperty - def list_url(cls) -> Optional[str]: # type: ignore[override] - if cls.pypi: - name = cls.pypi.split("/")[0] - return f"https://pypi.org/simple/{name}/" - return None + homepage: ClassProperty[Optional[str]] = classproperty(_homepage) + url: ClassProperty[Optional[str]] = classproperty(_url) + list_url: ClassProperty[Optional[str]] = classproperty(_list_url) @property def python_spec(self) -> Spec: diff --git a/lib/spack/spack/build_systems/r.py b/lib/spack/spack/build_systems/r.py index 1779acc1e05..87b1e104aeb 100644 --- a/lib/spack/spack/build_systems/r.py +++ b/lib/spack/spack/build_systems/r.py @@ -3,8 +3,8 @@ # SPDX-License-Identifier: (Apache-2.0 OR MIT) from typing import Optional, Tuple -import llnl.util.lang as lang from llnl.util.filesystem import mkdirp +from llnl.util.lang import ClassProperty, classproperty from spack.directives import extends @@ -54,6 +54,32 @@ def install(self, pkg, spec, prefix): pkg.module.R(*args) +def _homepage(cls: "RPackage") -> Optional[str]: + if cls.cran: + return f"https://cloud.r-project.org/package={cls.cran}" + elif cls.bioc: + return f"https://bioconductor.org/packages/{cls.bioc}" + return None + + +def _url(cls: "RPackage") -> Optional[str]: + if cls.cran: + return f"https://cloud.r-project.org/src/contrib/{cls.cran}_{str(list(cls.versions)[0])}.tar.gz" + return None + + +def _list_url(cls: "RPackage") -> Optional[str]: + if cls.cran: + return f"https://cloud.r-project.org/src/contrib/Archive/{cls.cran}/" + return None + + +def _git(cls: "RPackage") -> Optional[str]: + if cls.bioc: + return f"https://git.bioconductor.org/packages/{cls.bioc}" + return None + + class RPackage(Package): """Specialized class for packages that are built using R. @@ -77,24 +103,7 @@ class RPackage(Package): extends("r") - @lang.classproperty - def homepage(cls): - if cls.cran: - return f"https://cloud.r-project.org/package={cls.cran}" - elif cls.bioc: - return f"https://bioconductor.org/packages/{cls.bioc}" - - @lang.classproperty - def url(cls): - if cls.cran: - return f"https://cloud.r-project.org/src/contrib/{cls.cran}_{str(list(cls.versions)[0])}.tar.gz" - - @lang.classproperty - def list_url(cls): - if cls.cran: - return f"https://cloud.r-project.org/src/contrib/Archive/{cls.cran}/" - - @lang.classproperty - def git(cls): - if cls.bioc: - return f"https://git.bioconductor.org/packages/{cls.bioc}" + homepage: ClassProperty[Optional[str]] = classproperty(_homepage) + url: ClassProperty[Optional[str]] = classproperty(_url) + list_url: ClassProperty[Optional[str]] = classproperty(_list_url) + git: ClassProperty[Optional[str]] = classproperty(_git) diff --git a/lib/spack/spack/build_systems/racket.py b/lib/spack/spack/build_systems/racket.py index bd3988073e2..5ea5c9444d3 100644 --- a/lib/spack/spack/build_systems/racket.py +++ b/lib/spack/spack/build_systems/racket.py @@ -5,8 +5,8 @@ from typing import Optional, Tuple import llnl.util.filesystem as fs -import llnl.util.lang as lang import llnl.util.tty as tty +from llnl.util.lang import ClassProperty, classproperty import spack.builder import spack.spec @@ -19,6 +19,12 @@ from spack.util.executable import Executable, ProcessError +def _homepage(cls: "RacketPackage") -> Optional[str]: + if cls.racket_name: + return f"https://pkgs.racket-lang.org/package/{cls.racket_name}" + return None + + class RacketPackage(PackageBase): """Specialized class for packages that are built using Racket's `raco pkg install` and `raco setup` commands. @@ -37,13 +43,7 @@ class RacketPackage(PackageBase): extends("racket", when="build_system=racket") racket_name: Optional[str] = None - parallel = True - - @lang.classproperty - def homepage(cls): - if cls.racket_name: - return "https://pkgs.racket-lang.org/package/{0}".format(cls.racket_name) - return None + homepage: ClassProperty[Optional[str]] = classproperty(_homepage) @spack.builder.builder("racket") diff --git a/lib/spack/spack/package_base.py b/lib/spack/spack/package_base.py index d5b803b903f..54ba4bac32e 100644 --- a/lib/spack/spack/package_base.py +++ b/lib/spack/spack/package_base.py @@ -28,7 +28,7 @@ import llnl.util.filesystem as fsys import llnl.util.tty as tty -from llnl.util.lang import classproperty, memoized +from llnl.util.lang import ClassProperty, classproperty, memoized import spack.config import spack.dependency @@ -701,10 +701,10 @@ class PackageBase(WindowsRPath, PackageViewMixin, metaclass=PackageMeta): _verbose = None #: Package homepage where users can find more information about the package - homepage: Optional[str] = None + homepage: ClassProperty[Optional[str]] = None #: Default list URL (place to find available versions) - list_url: Optional[str] = None + list_url: ClassProperty[Optional[str]] = None #: Link depth to which list_url should be searched for new versions list_depth = 0