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
8677063142
commit
125feb125c
@ -13,6 +13,18 @@
|
|||||||
__version__ = "1.0.0.dev0"
|
__version__ = "1.0.0.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:
|
||||||
@ -79,4 +91,6 @@ def get_short_version() -> str:
|
|||||||
"get_version",
|
"get_version",
|
||||||
"get_spack_commit",
|
"get_spack_commit",
|
||||||
"get_short_version",
|
"get_short_version",
|
||||||
|
"package_api_version",
|
||||||
|
"min_package_api_version",
|
||||||
]
|
]
|
||||||
|
@ -32,6 +32,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,19 +912,52 @@ def __reduce__(self):
|
|||||||
return RepoPath.unmarshal, self.marshal()
|
return RepoPath.unmarshal, self.marshal()
|
||||||
|
|
||||||
|
|
||||||
|
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` 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.
|
||||||
|
|
||||||
`subdirectory`:
|
`subdirectory`:
|
||||||
An optional subdirectory name where packages are placed
|
An optional subdirectory name where packages are placed
|
||||||
|
|
||||||
|
`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__(
|
def __init__(
|
||||||
@ -958,7 +994,7 @@ def check(condition, msg):
|
|||||||
f"{os.path.join(root, repo_config_name)} must define a namespace.",
|
f"{os.path.join(root, repo_config_name)} must define a namespace.",
|
||||||
)
|
)
|
||||||
|
|
||||||
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),
|
||||||
f"Invalid namespace '{self.namespace}' in repo '{self.root}'. "
|
f"Invalid namespace '{self.namespace}' in repo '{self.root}'. "
|
||||||
@ -971,12 +1007,14 @@ 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), f"No directory '{packages_dir}' found in '{root}'"
|
os.path.isdir(self.packages_path), f"No directory '{packages_dir}' found in '{root}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.package_api = _parse_package_api_version(config)
|
||||||
|
|
||||||
# Class attribute overrides by package name
|
# Class attribute overrides by package name
|
||||||
self.overrides = overrides or {}
|
self.overrides = overrides or {}
|
||||||
|
|
||||||
@ -1026,7 +1064,7 @@ def is_prefix(self, fullname: str) -> bool:
|
|||||||
parts = fullname.split(".")
|
parts = fullname.split(".")
|
||||||
return self._names[: len(parts)] == parts
|
return self._names[: len(parts)] == parts
|
||||||
|
|
||||||
def _read_config(self) -> Dict[str, str]:
|
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, encoding="utf-8") as reponame_file:
|
with open(self.config_file, encoding="utf-8") as reponame_file:
|
||||||
|
@ -319,3 +319,48 @@ def test_get_repo(self, mock_test_cache):
|
|||||||
# foo is not there, raise
|
# foo is not there, raise
|
||||||
with pytest.raises(spack.repo.UnknownNamespaceError):
|
with pytest.raises(spack.repo.UnknownNamespaceError):
|
||||||
repo.get_repo("foo")
|
repo.get_repo("foo")
|
||||||
|
|
||||||
|
|
||||||
|
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: pathlib.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
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cache = spack.util.file_cache.FileCache(tmp_path / "cache")
|
||||||
|
assert spack.repo.Repo(str(tmp_path / "example"), cache=cache).package_api == (1, 0)
|
||||||
|
Loading…
Reference in New Issue
Block a user