Define Package API version (#49274)
Defines `spack.package_api_version` and `spack.min_package_api_version` as tuples (major, minor). This defines resp. the current Package API version implemented by this version of Spack and the minimal Package API version it is backwards compatible with. Repositories can optionally define: ```yaml repo: namespace: my_repo api: v1.2 ``` which indicates they are compatible with versions of Spack that implement Package API `>= 1.2` and `< 2.0`. When the `api` key is omitted, the default `v1.0` is assumed.
This commit is contained in:
parent
dd3ee5ffef
commit
9a6e51ccf0
@ -7,6 +7,18 @@
|
|||||||
__version__ = "0.22.6.dev0"
|
__version__ = "0.22.6.dev0"
|
||||||
spack_version = __version__
|
spack_version = __version__
|
||||||
|
|
||||||
|
#: The current Package API version implemented by this version of Spack. The Package API defines
|
||||||
|
#: the Python interface for packages as well as the layout of package repositories. The minor
|
||||||
|
#: version is incremented when the package API is extended in a backwards-compatible way. The major
|
||||||
|
#: version is incremented upon breaking changes. This version is changed independently from the
|
||||||
|
#: Spack version.
|
||||||
|
package_api_version = (1, 0)
|
||||||
|
|
||||||
|
#: The minimum Package API version that this version of Spack is compatible with. This should
|
||||||
|
#: always be a tuple of the form ``(major, 0)``, since compatibility with vX.Y implies
|
||||||
|
#: compatibility with vX.0.
|
||||||
|
min_package_api_version = (1, 0)
|
||||||
|
|
||||||
|
|
||||||
def __try_int(v):
|
def __try_int(v):
|
||||||
try:
|
try:
|
||||||
@ -19,4 +31,4 @@ def __try_int(v):
|
|||||||
spack_version_info = tuple([__try_int(v) for v in __version__.split(".")])
|
spack_version_info = tuple([__try_int(v) for v in __version__.split(".")])
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["spack_version_info", "spack_version"]
|
__all__ = ["spack_version_info", "spack_version", "package_api_version", "min_package_api_version"]
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from llnl.util.filesystem import working_dir
|
from llnl.util.filesystem import working_dir
|
||||||
|
|
||||||
|
import spack
|
||||||
import spack.caches
|
import spack.caches
|
||||||
import spack.config
|
import spack.config
|
||||||
import spack.error
|
import spack.error
|
||||||
@ -49,6 +50,8 @@
|
|||||||
#: Package modules are imported as spack.pkg.<repo-namespace>.<pkg-name>
|
#: Package modules are imported as spack.pkg.<repo-namespace>.<pkg-name>
|
||||||
ROOT_PYTHON_NAMESPACE = "spack.pkg"
|
ROOT_PYTHON_NAMESPACE = "spack.pkg"
|
||||||
|
|
||||||
|
_API_REGEX = re.compile(r"^v(\d+)\.(\d+)$")
|
||||||
|
|
||||||
|
|
||||||
def python_package_for_repo(namespace):
|
def python_package_for_repo(namespace):
|
||||||
"""Returns the full namespace of a repository, given its relative one
|
"""Returns the full namespace of a repository, given its relative one
|
||||||
@ -909,17 +912,49 @@ def __contains__(self, pkg_name):
|
|||||||
return self.exists(pkg_name)
|
return self.exists(pkg_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_package_api_version(
|
||||||
|
config: Dict[str, Any],
|
||||||
|
min_api: Tuple[int, int] = spack.min_package_api_version,
|
||||||
|
max_api: Tuple[int, int] = spack.package_api_version,
|
||||||
|
) -> Tuple[int, int]:
|
||||||
|
api = config.get("api")
|
||||||
|
if api is None:
|
||||||
|
package_api = (1, 0)
|
||||||
|
else:
|
||||||
|
if not isinstance(api, str):
|
||||||
|
raise BadRepoError(f"Invalid Package API version '{api}'. Must be of the form vX.Y")
|
||||||
|
api_match = _API_REGEX.match(api)
|
||||||
|
if api_match is None:
|
||||||
|
raise BadRepoError(f"Invalid Package API version '{api}'. Must be of the form vX.Y")
|
||||||
|
package_api = (int(api_match.group(1)), int(api_match.group(2)))
|
||||||
|
|
||||||
|
if min_api <= package_api <= max_api:
|
||||||
|
return package_api
|
||||||
|
|
||||||
|
min_str = ".".join(str(i) for i in min_api)
|
||||||
|
max_str = ".".join(str(i) for i in max_api)
|
||||||
|
curr_str = ".".join(str(i) for i in package_api)
|
||||||
|
raise BadRepoError(
|
||||||
|
f"Package API v{curr_str} is not supported by this version of Spack ("
|
||||||
|
f"must be between v{min_str} and v{max_str})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Repo:
|
class Repo:
|
||||||
"""Class representing a package repository in the filesystem.
|
"""Class representing a package repository in the filesystem.
|
||||||
|
|
||||||
Each package repository must have a top-level configuration file
|
Each package repository must have a top-level configuration file called `repo.yaml`.
|
||||||
called `repo.yaml`.
|
|
||||||
|
|
||||||
Currently, `repo.yaml` this must define:
|
It contains the following keys:
|
||||||
|
|
||||||
`namespace`:
|
`namespace`:
|
||||||
A Python namespace where the repository's packages should live.
|
A Python namespace where the repository's packages should live.
|
||||||
|
|
||||||
|
`api`:
|
||||||
|
A string of the form vX.Y that indicates the Package API version. The default is "v1.0".
|
||||||
|
For the repo to be compatible with the current version of Spack, the version must be
|
||||||
|
greater than or equal to :py:data:`spack.min_package_api_version` and less than or equal to
|
||||||
|
:py:data:`spack.package_api_version`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, root, cache=None):
|
def __init__(self, root, cache=None):
|
||||||
@ -948,7 +983,7 @@ def check(condition, msg):
|
|||||||
"%s must define a namespace." % os.path.join(root, repo_config_name),
|
"%s must define a namespace." % os.path.join(root, repo_config_name),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.namespace = config["namespace"]
|
self.namespace: str = config["namespace"]
|
||||||
check(
|
check(
|
||||||
re.match(r"[a-zA-Z][a-zA-Z0-9_.]+", self.namespace),
|
re.match(r"[a-zA-Z][a-zA-Z0-9_.]+", self.namespace),
|
||||||
("Invalid namespace '%s' in repo '%s'. " % (self.namespace, self.root))
|
("Invalid namespace '%s' in repo '%s'. " % (self.namespace, self.root))
|
||||||
@ -961,13 +996,15 @@ def check(condition, msg):
|
|||||||
# Keep name components around for checking prefixes.
|
# Keep name components around for checking prefixes.
|
||||||
self._names = self.full_namespace.split(".")
|
self._names = self.full_namespace.split(".")
|
||||||
|
|
||||||
packages_dir = config.get("subdirectory", packages_dir_name)
|
packages_dir: str = config.get("subdirectory", packages_dir_name)
|
||||||
self.packages_path = os.path.join(self.root, packages_dir)
|
self.packages_path = os.path.join(self.root, packages_dir)
|
||||||
check(
|
check(
|
||||||
os.path.isdir(self.packages_path),
|
os.path.isdir(self.packages_path),
|
||||||
"No directory '%s' found in '%s'" % (packages_dir, root),
|
"No directory '%s' found in '%s'" % (packages_dir, root),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.package_api = _parse_package_api_version(config)
|
||||||
|
|
||||||
# These are internal cache variables.
|
# These are internal cache variables.
|
||||||
self._modules = {}
|
self._modules = {}
|
||||||
self._classes = {}
|
self._classes = {}
|
||||||
@ -1010,7 +1047,7 @@ def is_prefix(self, fullname):
|
|||||||
parts = fullname.split(".")
|
parts = fullname.split(".")
|
||||||
return self._names[: len(parts)] == parts
|
return self._names[: len(parts)] == parts
|
||||||
|
|
||||||
def _read_config(self):
|
def _read_config(self) -> Dict[str, Any]:
|
||||||
"""Check for a YAML config file in this db's root directory."""
|
"""Check for a YAML config file in this db's root directory."""
|
||||||
try:
|
try:
|
||||||
with open(self.config_file) as reponame_file:
|
with open(self.config_file) as reponame_file:
|
||||||
|
@ -195,3 +195,47 @@ def test_path_computation_with_names(method_name, mock_repo_path):
|
|||||||
unqualified = method("mpileaks")
|
unqualified = method("mpileaks")
|
||||||
qualified = method("builtin.mock.mpileaks")
|
qualified = method("builtin.mock.mpileaks")
|
||||||
assert qualified == unqualified
|
assert qualified == unqualified
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_package_api_version():
|
||||||
|
"""Test that we raise an error if a repository has a version that is not supported."""
|
||||||
|
# valid version
|
||||||
|
assert spack.repo._parse_package_api_version(
|
||||||
|
{"api": "v1.2"}, min_api=(1, 0), max_api=(2, 3)
|
||||||
|
) == (1, 2)
|
||||||
|
# too new and too old
|
||||||
|
with pytest.raises(
|
||||||
|
spack.repo.BadRepoError,
|
||||||
|
match=r"Package API v2.4 is not supported .* \(must be between v1.0 and v2.3\)",
|
||||||
|
):
|
||||||
|
spack.repo._parse_package_api_version({"api": "v2.4"}, min_api=(1, 0), max_api=(2, 3))
|
||||||
|
with pytest.raises(
|
||||||
|
spack.repo.BadRepoError,
|
||||||
|
match=r"Package API v0.9 is not supported .* \(must be between v1.0 and v2.3\)",
|
||||||
|
):
|
||||||
|
spack.repo._parse_package_api_version({"api": "v0.9"}, min_api=(1, 0), max_api=(2, 3))
|
||||||
|
# default to v1.0 if not specified
|
||||||
|
assert spack.repo._parse_package_api_version({}, min_api=(1, 0), max_api=(2, 3)) == (1, 0)
|
||||||
|
# if v1.0 support is dropped we should also raise
|
||||||
|
with pytest.raises(
|
||||||
|
spack.repo.BadRepoError,
|
||||||
|
match=r"Package API v1.0 is not supported .* \(must be between v2.0 and v2.3\)",
|
||||||
|
):
|
||||||
|
spack.repo._parse_package_api_version({}, min_api=(2, 0), max_api=(2, 3))
|
||||||
|
# finally test invalid input
|
||||||
|
with pytest.raises(spack.repo.BadRepoError, match="Invalid Package API version"):
|
||||||
|
spack.repo._parse_package_api_version({"api": "v2"}, min_api=(1, 0), max_api=(3, 3))
|
||||||
|
with pytest.raises(spack.repo.BadRepoError, match="Invalid Package API version"):
|
||||||
|
spack.repo._parse_package_api_version({"api": 2.0}, min_api=(1, 0), max_api=(3, 3))
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_package_api_version(tmp_path):
|
||||||
|
"""Test that we can specify the API version of a repository."""
|
||||||
|
(tmp_path / "example" / "packages").mkdir(parents=True)
|
||||||
|
(tmp_path / "example" / "repo.yaml").write_text(
|
||||||
|
"""\
|
||||||
|
repo:
|
||||||
|
namespace: example
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert spack.repo.Repo(str(tmp_path / "example"), cache=None).package_api == (1, 0)
|
||||||
|
Loading…
Reference in New Issue
Block a user