Support for prereleases (#43140)

This adds support for prereleases. Alpha, beta and release candidate
suffixes are ordered in the intuitive way:

```
1.2.0-alpha < 1.2.0-alpha.1 < 1.2.0-beta.2 < 1.2.0-rc.3 < 1.2.0 < 1.2.0-xyz
```

Alpha, beta and rc prereleases are defined as follows: split the version
string into components like before (on delimiters and string boundaries).
If there's a string component `alpha`, `beta` or `rc` followed by an optional
numeric component at the end, then the version is prerelease.

So `1.2.0-alpha.1 == 1.2.0alpha1 == 1.2.0.alpha1` are all the same, as usual.

The strings `alpha`, `beta` and `rc` are chosen because they match semver,
they are sufficiently long to be unambiguous, and and all contain at least
one non-hex character so distinguish them from shasum/digest type suffixes.

The comparison key is now stored as `(release_tuple, prerelease_tuple)`, so in
the above example:

```
((1,2,0),(ALPHA,)) < ((1,2,0),(ALPHA,1)) < ((1,2,0),(BETA,2)) < ((1,2,0),(RC,3)) < ((1,2,0),(FINAL,)) < ((1,2,0,"xyz"), (FINAL,))
```

The version ranges `@1.2.0:` and `@:1.1` do *not* include prereleases of
`1.2.0`.

So for packaging, if the `1.2.0alpha` and `1.2.0` versions have the same constraints on
dependencies, it's best to write

```python
depends_on("x@1:", when="@1.2.0alpha:")
```

However, `@1.2:` does include `1.2.0alpha`. This is because Spack considers
`1.2 < 1.2.0` as distinct versions, with `1.2 < 1.2.0alpha < 1.2.0` as a consequence.

Alternatively, the above `depends_on` statement can thus be written

```python
depends_on("x@1:", when="@1.2:")
```

which can be useful too. A short-hand to include prereleases, but you
can still be explicit to exclude the prerelease by specifying the patch version
number.

### Concretization

Concretization uses a different version order than `<`. Prereleases are ordered
between final releases and develop versions. That way, users should not
have to set `preferred=True` on every final release if they add just one
prerelease to a package. The concretizer is unlikely to pick a prerelease when
final releases are possible.

### Limitations

1. You can't express a range that includes all alpha release but excludes all beta
   releases. Only alternative is good old repeated nines: `@:1.2.0alpha99`.

2. The Python ecosystem defaults to `a`, `b`, `rc` strings, so translation of Python versions to
   Spack versions requires expansion to `alpha`, `beta`, `rc`. It's mildly annoying, because
   this means we may need to compute URLs differently (not done in this commit).

### Hash

Care is taken not to break hashes of versions that do not have a prerelease
suffix.
This commit is contained in:
Harmen Stoppels 2024-03-22 23:30:32 +01:00 committed by GitHub
parent 397334a4be
commit c3eaf4d6cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 270 additions and 110 deletions

View File

@ -1119,6 +1119,9 @@ and ``3.4.2``. Similarly, ``@4.2:`` means any version above and including
``4.2``. As a short-hand, ``@3`` is equivalent to the range ``@3:3`` and ``4.2``. As a short-hand, ``@3`` is equivalent to the range ``@3:3`` and
includes any version with major version ``3``. includes any version with major version ``3``.
Versions are ordered lexicograpically by its components. For more details
on the order, see :ref:`the packaging guide <version-comparison>`.
Notice that you can distinguish between the specific version ``@=3.2`` and Notice that you can distinguish between the specific version ``@=3.2`` and
the range ``@3.2``. This is useful for packages that follow a versioning the range ``@3.2``. This is useful for packages that follow a versioning
scheme that omits the zero patch version number: ``3.2``, ``3.2.1``, scheme that omits the zero patch version number: ``3.2``, ``3.2.1``,

View File

@ -893,26 +893,50 @@ as an option to the ``version()`` directive. Example situations would be a
"snapshot"-like Version Control System (VCS) tag, a VCS branch such as "snapshot"-like Version Control System (VCS) tag, a VCS branch such as
``v6-16-00-patches``, or a URL specifying a regularly updated snapshot tarball. ``v6-16-00-patches``, or a URL specifying a regularly updated snapshot tarball.
.. _version-comparison:
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
Version comparison Version comparison
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
Spack imposes a generic total ordering on the set of versions,
independently from the package they are associated with.
Most Spack versions are numeric, a tuple of integers; for example, Most Spack versions are numeric, a tuple of integers; for example,
``0.1``, ``6.96`` or ``1.2.3.1``. Spack knows how to compare and sort ``0.1``, ``6.96`` or ``1.2.3.1``. In this very basic case, version
numeric versions. comparison is lexicographical on the numeric components:
``1.2 < 1.2.1 < 1.2.2 < 1.10``.
Some Spack versions involve slight extensions of numeric syntax; for Spack can also supports string components such as ``1.1.1a`` and
example, ``py-sphinx-rtd-theme@=0.1.10a0``. In this case, numbers are ``1.y.0``. String components are considered less than numeric
always considered to be "newer" than letters. This is for consistency components, so ``1.y.0 < 1.0``. This is for consistency with
with `RPM <https://bugzilla.redhat.com/show_bug.cgi?id=50977>`_. `RPM <https://bugzilla.redhat.com/show_bug.cgi?id=50977>`_. String
components do not have to be separated by dots or any other delimiter.
So, the contrived version ``1y0`` is identical to ``1.y.0``.
Spack versions may also be arbitrary non-numeric strings, for example Pre-release suffixes also contain string parts, but they are handled
``develop``, ``master``, ``local``. in a special way. For example ``1.2.3alpha1`` is parsed as a pre-release
of the version ``1.2.3``. This allows Spack to order it before the
actual release: ``1.2.3alpha1 < 1.2.3``. Spack supports alpha, beta and
release candidate suffixes: ``1.2alpha1 < 1.2beta1 < 1.2rc1 < 1.2``. Any
suffix not recognized as a pre-release is treated as an ordinary
string component, so ``1.2 < 1.2-mysuffix``.
The order on versions is defined as follows. A version string is split Finally, there are a few special string components that are considered
into a list of components based on delimiters such as ``.``, ``-`` etc. "infinity versions". They include ``develop``, ``main``, ``master``,
Lists are then ordered lexicographically, where components are ordered ``head``, ``trunk``, and ``stable``. For example: ``1.2 < develop``.
as follows: These are useful for specifying the most recent development version of
a package (often a moving target like a git branch), without assigning
a specific version number. Infinity versions are not automatically used when determining the latest version of a package unless explicitly required by another package or user.
More formally, the order on versions is defined as follows. A version
string is split into a list of components based on delimiters such as
``.`` and ``-`` and string boundaries. The components are split into
the **release** and a possible **pre-release** (if the last component
is numeric and the second to last is a string ``alpha``, ``beta`` or ``rc``).
The release components are ordered lexicographically, with comparsion
between different types of components as follows:
#. The following special strings are considered larger than any other #. The following special strings are considered larger than any other
numeric or non-numeric version component, and satisfy the following numeric or non-numeric version component, and satisfy the following
@ -925,6 +949,9 @@ as follows:
#. All other non-numeric components are less than numeric components, #. All other non-numeric components are less than numeric components,
and are ordered alphabetically. and are ordered alphabetically.
Finally, if the release components are equal, the pre-release components
are used to break the tie, in the obvious way.
The logic behind this sort order is two-fold: The logic behind this sort order is two-fold:
#. Non-numeric versions are usually used for special cases while #. Non-numeric versions are usually used for special cases while

View File

@ -541,6 +541,7 @@ def _concretization_version_order(version_info: Tuple[GitOrStandardVersion, dict
info.get("preferred", False), info.get("preferred", False),
not info.get("deprecated", False), not info.get("deprecated", False),
not version.isdevelop(), not version.isdevelop(),
not version.is_prerelease(),
version, version,
) )
@ -2158,7 +2159,7 @@ def versions_for(v):
if isinstance(v, vn.StandardVersion): if isinstance(v, vn.StandardVersion):
return [v] return [v]
elif isinstance(v, vn.ClosedOpenRange): elif isinstance(v, vn.ClosedOpenRange):
return [v.lo, vn.prev_version(v.hi)] return [v.lo, vn._prev_version(v.hi)]
elif isinstance(v, vn.VersionList): elif isinstance(v, vn.VersionList):
return sum((versions_for(e) for e in v), []) return sum((versions_for(e) for e in v), [])
else: else:

View File

@ -155,5 +155,6 @@ def optimization_flags(self, compiler):
# log this and just return compiler.version instead # log this and just return compiler.version instead
tty.debug(str(e)) tty.debug(str(e))
compiler_version = compiler_version.dotted.force_numeric return self.microarchitecture.optimization_flags(
return self.microarchitecture.optimization_flags(compiler.name, str(compiler_version)) compiler.name, compiler_version.dotted_numeric_string
)

View File

@ -2613,3 +2613,28 @@ def test_reusable_externals_different_spec(mock_packages, tmpdir):
{"mpich": {"externals": [{"spec": "mpich@4.1 +debug", "prefix": tmpdir.strpath}]}}, {"mpich": {"externals": [{"spec": "mpich@4.1 +debug", "prefix": tmpdir.strpath}]}},
local=False, local=False,
) )
def test_concretization_version_order():
versions = [
(Version("develop"), {}),
(Version("1.0"), {}),
(Version("2.0"), {"deprecated": True}),
(Version("1.1"), {}),
(Version("1.1alpha1"), {}),
(Version("0.9"), {"preferred": True}),
]
result = [
v
for v, _ in sorted(
versions, key=spack.solver.asp._concretization_version_order, reverse=True
)
]
assert result == [
Version("0.9"), # preferred
Version("1.1"), # latest non-deprecated final version
Version("1.0"), # latest non-deprecated final version
Version("1.1alpha1"), # prereleases
Version("develop"), # likely development version
Version("2.0"), # deprecated
]

View File

@ -210,16 +210,10 @@ def test_from_list_url(mock_packages, config, spec, url, digest, _fetch_method):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"requested_version,tarball,digest", "requested_version,tarball,digest",
[ [
# This version is in the web data path (test/data/web/4.html), but not in the # These versions are in the web data path (test/data/web/4.html), but not in the
# url-list-test package. We expect Spack to generate a URL with the new version. # url-list-test package. We expect Spack to generate a URL with the new version.
("=4.5.0", "foo-4.5.0.tar.gz", None), ("=4.5.0", "foo-4.5.0.tar.gz", None),
# This version is in web data path and not in the package file, BUT the 2.0.0b2 ("=2.0.0", "foo-2.0.0.tar.gz", None),
# version in the package file satisfies 2.0.0, so Spack will use the known version.
# TODO: this is *probably* not what the user wants, but it's here as an example
# TODO: for that reason. We can't express "exactly 2.0.0" right now, and we don't
# TODO: have special cases that would make 2.0.0b2 less than 2.0.0. We should
# TODO: probably revisit this in our versioning scheme.
("2.0.0", "foo-2.0.0b2.tar.gz", "000000000000000000000000000200b2"),
], ],
) )
@pytest.mark.only_clingo("Original concretizer doesn't resolve concrete versions to known ones") @pytest.mark.only_clingo("Original concretizer doesn't resolve concrete versions to known ones")
@ -228,7 +222,7 @@ def test_new_version_from_list_url(
): ):
"""Test non-specific URLs from the url-list-test package.""" """Test non-specific URLs from the url-list-test package."""
with spack.config.override("config:url_fetch_method", _fetch_method): with spack.config.override("config:url_fetch_method", _fetch_method):
s = Spec("url-list-test @%s" % requested_version).concretized() s = Spec(f"url-list-test @{requested_version}").concretized()
fetch_strategy = fs.from_list_url(s.package) fetch_strategy = fs.from_list_url(s.package)
assert isinstance(fetch_strategy, fs.URLFetchStrategy) assert isinstance(fetch_strategy, fs.URLFetchStrategy)

View File

@ -213,12 +213,24 @@ def test_nums_and_patch():
assert_ver_gt("=6.5p1", "=5.6p1") assert_ver_gt("=6.5p1", "=5.6p1")
def test_rc_versions(): def test_prereleases():
assert_ver_gt("=6.0.rc1", "=6.0") # pre-releases are special: they are less than final releases
assert_ver_lt("=6.0", "=6.0.rc1") assert_ver_lt("=6.0alpha", "=6.0alpha0")
assert_ver_lt("=6.0alpha0", "=6.0alpha1")
assert_ver_lt("=6.0alpha1", "=6.0alpha2")
assert_ver_lt("=6.0alpha2", "=6.0beta")
assert_ver_lt("=6.0beta", "=6.0beta0")
assert_ver_lt("=6.0beta0", "=6.0beta1")
assert_ver_lt("=6.0beta1", "=6.0beta2")
assert_ver_lt("=6.0beta2", "=6.0rc")
assert_ver_lt("=6.0rc", "=6.0rc0")
assert_ver_lt("=6.0rc0", "=6.0rc1")
assert_ver_lt("=6.0rc1", "=6.0rc2")
assert_ver_lt("=6.0rc2", "=6.0")
def test_alpha_beta(): def test_alpha_beta():
# these are not pre-releases, but ordinary string components.
assert_ver_gt("=10b2", "=10a1") assert_ver_gt("=10b2", "=10a1")
assert_ver_lt("=10a2", "=10b2") assert_ver_lt("=10a2", "=10b2")
@ -277,6 +289,39 @@ def test_version_ranges():
assert_ver_gt("1.5:1.6", "1.2:1.4") assert_ver_gt("1.5:1.6", "1.2:1.4")
def test_version_range_with_prereleases():
# 1.2.1: means from the 1.2.1 release onwards
assert_does_not_satisfy("1.2.1alpha1", "1.2.1:")
assert_does_not_satisfy("1.2.1beta2", "1.2.1:")
assert_does_not_satisfy("1.2.1rc3", "1.2.1:")
# Pre-releases of 1.2.1 are included in the 1.2.0: range
assert_satisfies("1.2.1alpha1", "1.2.0:")
assert_satisfies("1.2.1beta1", "1.2.0:")
assert_satisfies("1.2.1rc3", "1.2.0:")
# In Spack 1.2 and 1.2.0 are distinct with 1.2 < 1.2.0. So a lowerbound on 1.2 includes
# pre-releases of 1.2.0 as well.
assert_satisfies("1.2.0alpha1", "1.2:")
assert_satisfies("1.2.0beta2", "1.2:")
assert_satisfies("1.2.0rc3", "1.2:")
# An upperbound :1.1 does not include 1.2.0 pre-releases
assert_does_not_satisfy("1.2.0alpha1", ":1.1")
assert_does_not_satisfy("1.2.0beta2", ":1.1")
assert_does_not_satisfy("1.2.0rc3", ":1.1")
assert_satisfies("1.2.0alpha1", ":1.2")
assert_satisfies("1.2.0beta2", ":1.2")
assert_satisfies("1.2.0rc3", ":1.2")
# You can also construct ranges from prereleases
assert_satisfies("1.2.0alpha2:1.2.0beta1", "1.2.0alpha1:1.2.0beta2")
assert_satisfies("1.2.0", "1.2.0alpha1:")
assert_satisfies("=1.2.0", "1.2.0alpha1:")
assert_does_not_satisfy("=1.2.0", ":1.2.0rc345")
def test_contains(): def test_contains():
assert_in("=1.3", "1.2:1.4") assert_in("=1.3", "1.2:1.4")
assert_in("=1.2.5", "1.2:1.4") assert_in("=1.2.5", "1.2:1.4")
@ -417,12 +462,12 @@ def test_basic_version_satisfaction():
assert_satisfies("4.7.3", "4.7.3") assert_satisfies("4.7.3", "4.7.3")
assert_satisfies("4.7.3", "4.7") assert_satisfies("4.7.3", "4.7")
assert_satisfies("4.7.3b2", "4.7") assert_satisfies("4.7.3v2", "4.7")
assert_satisfies("4.7b6", "4.7") assert_satisfies("4.7v6", "4.7")
assert_satisfies("4.7.3", "4") assert_satisfies("4.7.3", "4")
assert_satisfies("4.7.3b2", "4") assert_satisfies("4.7.3v2", "4")
assert_satisfies("4.7b6", "4") assert_satisfies("4.7v6", "4")
assert_does_not_satisfy("4.8.0", "4.9") assert_does_not_satisfy("4.8.0", "4.9")
assert_does_not_satisfy("4.8", "4.9") assert_does_not_satisfy("4.8", "4.9")
@ -433,12 +478,12 @@ def test_basic_version_satisfaction_in_lists():
assert_satisfies(["4.7.3"], ["4.7.3"]) assert_satisfies(["4.7.3"], ["4.7.3"])
assert_satisfies(["4.7.3"], ["4.7"]) assert_satisfies(["4.7.3"], ["4.7"])
assert_satisfies(["4.7.3b2"], ["4.7"]) assert_satisfies(["4.7.3v2"], ["4.7"])
assert_satisfies(["4.7b6"], ["4.7"]) assert_satisfies(["4.7v6"], ["4.7"])
assert_satisfies(["4.7.3"], ["4"]) assert_satisfies(["4.7.3"], ["4"])
assert_satisfies(["4.7.3b2"], ["4"]) assert_satisfies(["4.7.3v2"], ["4"])
assert_satisfies(["4.7b6"], ["4"]) assert_satisfies(["4.7v6"], ["4"])
assert_does_not_satisfy(["4.8.0"], ["4.9"]) assert_does_not_satisfy(["4.8.0"], ["4.9"])
assert_does_not_satisfy(["4.8"], ["4.9"]) assert_does_not_satisfy(["4.8"], ["4.9"])
@ -507,6 +552,11 @@ def test_formatted_strings():
assert v.dotted.joined.string == "123b" assert v.dotted.joined.string == "123b"
def test_dotted_numeric_string():
assert Version("1a2b3").dotted_numeric_string == "1.0.2.0.3"
assert Version("1a2b3alpha4").dotted_numeric_string == "1.0.2.0.3.0.4"
def test_up_to(): def test_up_to():
v = Version("1.23-4_5b") v = Version("1.23-4_5b")
@ -548,9 +598,18 @@ def check_repr_and_str(vrs):
check_repr_and_str("R2016a.2-3_4") check_repr_and_str("R2016a.2-3_4")
@pytest.mark.parametrize(
"version_str", ["1.2string3", "1.2-3xyz_4-alpha.5", "1.2beta", "1_x_rc-4"]
)
def test_stringify_version(version_str):
v = Version(version_str)
v.string = None
assert str(v) == version_str
def test_len(): def test_len():
a = Version("1.2.3.4") a = Version("1.2.3.4")
assert len(a) == len(a.version) assert len(a) == len(a.version[0])
assert len(a) == 4 assert len(a) == 4
b = Version("2018.0") b = Version("2018.0")
assert len(b) == 2 assert len(b) == 2

View File

@ -30,9 +30,9 @@
Version, Version,
VersionList, VersionList,
VersionRange, VersionRange,
_next_version,
_prev_version,
from_string, from_string,
next_version,
prev_version,
ver, ver,
) )
@ -46,8 +46,8 @@
"from_string", "from_string",
"is_git_version", "is_git_version",
"infinity_versions", "infinity_versions",
"prev_version", "_prev_version",
"next_version", "_next_version",
"VersionList", "VersionList",
"ClosedOpenRange", "ClosedOpenRange",
"StandardVersion", "StandardVersion",

View File

@ -15,6 +15,14 @@
iv_min_len = min(len(s) for s in infinity_versions) iv_min_len = min(len(s) for s in infinity_versions)
ALPHA = 0
BETA = 1
RC = 2
FINAL = 3
PRERELEASE_TO_STRING = ["alpha", "beta", "rc"]
STRING_TO_PRERELEASE = {"alpha": ALPHA, "beta": BETA, "rc": RC, "final": FINAL}
def is_git_version(string: str) -> bool: def is_git_version(string: str) -> bool:
return ( return (

View File

@ -11,7 +11,11 @@
from spack.util.spack_yaml import syaml_dict from spack.util.spack_yaml import syaml_dict
from .common import ( from .common import (
ALPHA,
COMMIT_VERSION, COMMIT_VERSION,
FINAL,
PRERELEASE_TO_STRING,
STRING_TO_PRERELEASE,
EmptyRangeError, EmptyRangeError,
VersionLookupError, VersionLookupError,
infinity_versions, infinity_versions,
@ -88,21 +92,50 @@ def parse_string_components(string: str) -> Tuple[tuple, tuple]:
raise ValueError("Bad characters in version string: %s" % string) raise ValueError("Bad characters in version string: %s" % string)
segments = SEGMENT_REGEX.findall(string) segments = SEGMENT_REGEX.findall(string)
version = tuple(int(m[0]) if m[0] else VersionStrComponent.from_string(m[1]) for m in segments)
separators = tuple(m[2] for m in segments) separators = tuple(m[2] for m in segments)
return version, separators prerelease: Tuple[int, ...]
# <version>(alpha|beta|rc)<number>
if len(segments) >= 3 and segments[-2][1] in STRING_TO_PRERELEASE and segments[-1][0]:
prerelease = (STRING_TO_PRERELEASE[segments[-2][1]], int(segments[-1][0]))
segments = segments[:-2]
# <version>(alpha|beta|rc)
elif len(segments) >= 2 and segments[-1][1] in STRING_TO_PRERELEASE:
prerelease = (STRING_TO_PRERELEASE[segments[-1][1]],)
segments = segments[:-1]
# <version>
else:
prerelease = (FINAL,)
release = tuple(int(m[0]) if m[0] else VersionStrComponent.from_string(m[1]) for m in segments)
return (release, prerelease), separators
class ConcreteVersion: class ConcreteVersion:
pass pass
def _stringify_version(versions: Tuple[tuple, tuple], separators: tuple) -> str:
release, prerelease = versions
string = ""
for i in range(len(release)):
string += f"{release[i]}{separators[i]}"
if prerelease[0] != FINAL:
string += f"{PRERELEASE_TO_STRING[prerelease[0]]}{separators[len(release)]}"
if len(prerelease) > 1:
string += str(prerelease[1])
return string
class StandardVersion(ConcreteVersion): class StandardVersion(ConcreteVersion):
"""Class to represent versions""" """Class to represent versions"""
__slots__ = ["version", "string", "separators"] __slots__ = ["version", "string", "separators"]
def __init__(self, string: Optional[str], version: tuple, separators: tuple): def __init__(self, string: Optional[str], version: Tuple[tuple, tuple], separators: tuple):
self.string = string self.string = string
self.version = version self.version = version
self.separators = separators self.separators = separators
@ -113,11 +146,13 @@ def from_string(string: str):
@staticmethod @staticmethod
def typemin(): def typemin():
return StandardVersion("", (), ()) return StandardVersion("", ((), (ALPHA,)), ("",))
@staticmethod @staticmethod
def typemax(): def typemax():
return StandardVersion("infinity", (VersionStrComponent(len(infinity_versions)),), ()) return StandardVersion(
"infinity", ((VersionStrComponent(len(infinity_versions)),), (FINAL,)), ("",)
)
def __bool__(self): def __bool__(self):
return True return True
@ -164,21 +199,23 @@ def __gt__(self, other):
return NotImplemented return NotImplemented
def __iter__(self): def __iter__(self):
return iter(self.version) return iter(self.version[0])
def __len__(self): def __len__(self):
return len(self.version) return len(self.version[0])
def __getitem__(self, idx): def __getitem__(self, idx):
cls = type(self) cls = type(self)
release = self.version[0]
if isinstance(idx, numbers.Integral): if isinstance(idx, numbers.Integral):
return self.version[idx] return release[idx]
elif isinstance(idx, slice): elif isinstance(idx, slice):
string_arg = [] string_arg = []
pairs = zip(self.version[idx], self.separators[idx]) pairs = zip(release[idx], self.separators[idx])
for token, sep in pairs: for token, sep in pairs:
string_arg.append(str(token)) string_arg.append(str(token))
string_arg.append(str(sep)) string_arg.append(str(sep))
@ -193,22 +230,16 @@ def __getitem__(self, idx):
message = "{cls.__name__} indices must be integers" message = "{cls.__name__} indices must be integers"
raise TypeError(message.format(cls=cls)) raise TypeError(message.format(cls=cls))
def _stringify(self):
string = ""
for index in range(len(self.version)):
string += str(self.version[index])
string += str(self.separators[index])
return string
def __str__(self): def __str__(self):
return self.string or self._stringify() return self.string or _stringify_version(self.version, self.separators)
def __repr__(self) -> str: def __repr__(self) -> str:
# Print indirect repr through Version(...) # Print indirect repr through Version(...)
return f'Version("{str(self)}")' return f'Version("{str(self)}")'
def __hash__(self): def __hash__(self):
return hash(self.version) # If this is a final release, do not hash the prerelease part for backward compat.
return hash(self.version if self.is_prerelease() else self.version[0])
def __contains__(rhs, lhs): def __contains__(rhs, lhs):
# We should probably get rid of `x in y` for versions, since # We should probably get rid of `x in y` for versions, since
@ -257,23 +288,22 @@ def intersection(self, other: Union["ClosedOpenRange", "StandardVersion"]):
def isdevelop(self): def isdevelop(self):
"""Triggers on the special case of the `@develop-like` version.""" """Triggers on the special case of the `@develop-like` version."""
return any( return any(
isinstance(p, VersionStrComponent) and isinstance(p.data, int) for p in self.version isinstance(p, VersionStrComponent) and isinstance(p.data, int) for p in self.version[0]
) )
def is_prerelease(self) -> bool:
return self.version[1][0] != FINAL
@property @property
def force_numeric(self): def dotted_numeric_string(self) -> str:
"""Replaces all non-numeric components of the version with 0 """Replaces all non-numeric components of the version with 0.
This can be used to pass Spack versions to libraries that have stricter version schema. This can be used to pass Spack versions to libraries that have stricter version schema.
""" """
numeric = tuple(0 if isinstance(v, VersionStrComponent) else v for v in self.version) numeric = tuple(0 if isinstance(v, VersionStrComponent) else v for v in self.version[0])
# null separators except the final one have to be converted to avoid concatenating ints if self.is_prerelease():
# default to '.' as most common delimiter for versions numeric += (0, *self.version[1][1:])
separators = tuple( return ".".join(str(v) for v in numeric)
"." if s == "" and i != len(self.separators) - 1 else s
for i, s in enumerate(self.separators)
)
return type(self)(None, numeric, separators)
@property @property
def dotted(self): def dotted(self):
@ -591,6 +621,9 @@ def __getitem__(self, idx):
def isdevelop(self): def isdevelop(self):
return self.ref_version.isdevelop() return self.ref_version.isdevelop()
def is_prerelease(self) -> bool:
return self.ref_version.is_prerelease()
@property @property
def dotted(self) -> StandardVersion: def dotted(self) -> StandardVersion:
return self.ref_version.dotted return self.ref_version.dotted
@ -622,14 +655,14 @@ def __init__(self, lo: StandardVersion, hi: StandardVersion):
def from_version_range(cls, lo: StandardVersion, hi: StandardVersion): def from_version_range(cls, lo: StandardVersion, hi: StandardVersion):
"""Construct ClosedOpenRange from lo:hi range.""" """Construct ClosedOpenRange from lo:hi range."""
try: try:
return ClosedOpenRange(lo, next_version(hi)) return ClosedOpenRange(lo, _next_version(hi))
except EmptyRangeError as e: except EmptyRangeError as e:
raise EmptyRangeError(f"{lo}:{hi} is an empty range") from e raise EmptyRangeError(f"{lo}:{hi} is an empty range") from e
def __str__(self): def __str__(self):
# This simplifies 3.1:<3.2 to 3.1:3.1 to 3.1 # This simplifies 3.1:<3.2 to 3.1:3.1 to 3.1
# 3:3 -> 3 # 3:3 -> 3
hi_prev = prev_version(self.hi) hi_prev = _prev_version(self.hi)
if self.lo != StandardVersion.typemin() and self.lo == hi_prev: if self.lo != StandardVersion.typemin() and self.lo == hi_prev:
return str(self.lo) return str(self.lo)
lhs = "" if self.lo == StandardVersion.typemin() else str(self.lo) lhs = "" if self.lo == StandardVersion.typemin() else str(self.lo)
@ -641,7 +674,7 @@ def __repr__(self):
def __hash__(self): def __hash__(self):
# prev_version for backward compat. # prev_version for backward compat.
return hash((self.lo, prev_version(self.hi))) return hash((self.lo, _prev_version(self.hi)))
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, StandardVersion): if isinstance(other, StandardVersion):
@ -823,7 +856,7 @@ def concrete_range_as_version(self) -> Optional[ConcreteVersion]:
v = self[0] v = self[0]
if isinstance(v, ConcreteVersion): if isinstance(v, ConcreteVersion):
return v return v
if isinstance(v, ClosedOpenRange) and next_version(v.lo) == v.hi: if isinstance(v, ClosedOpenRange) and _next_version(v.lo) == v.hi:
return v.lo return v.lo
return None return None
@ -994,7 +1027,7 @@ def __repr__(self):
return str(self.versions) return str(self.versions)
def next_str(s: str) -> str: def _next_str(s: str) -> str:
"""Produce the next string of A-Z and a-z characters""" """Produce the next string of A-Z and a-z characters"""
return ( return (
(s + "A") (s + "A")
@ -1003,7 +1036,7 @@ def next_str(s: str) -> str:
) )
def prev_str(s: str) -> str: def _prev_str(s: str) -> str:
"""Produce the previous string of A-Z and a-z characters""" """Produce the previous string of A-Z and a-z characters"""
return ( return (
s[:-1] s[:-1]
@ -1012,7 +1045,7 @@ def prev_str(s: str) -> str:
) )
def next_version_str_component(v: VersionStrComponent) -> VersionStrComponent: def _next_version_str_component(v: VersionStrComponent) -> VersionStrComponent:
""" """
Produce the next VersionStrComponent, where Produce the next VersionStrComponent, where
masteq -> mastes masteq -> mastes
@ -1025,14 +1058,14 @@ def next_version_str_component(v: VersionStrComponent) -> VersionStrComponent:
# Find the next non-infinity string. # Find the next non-infinity string.
while True: while True:
data = next_str(data) data = _next_str(data)
if data not in infinity_versions: if data not in infinity_versions:
break break
return VersionStrComponent(data) return VersionStrComponent(data)
def prev_version_str_component(v: VersionStrComponent) -> VersionStrComponent: def _prev_version_str_component(v: VersionStrComponent) -> VersionStrComponent:
""" """
Produce the previous VersionStrComponent, where Produce the previous VersionStrComponent, where
mastes -> masteq mastes -> masteq
@ -1045,47 +1078,56 @@ def prev_version_str_component(v: VersionStrComponent) -> VersionStrComponent:
# Find the next string. # Find the next string.
while True: while True:
data = prev_str(data) data = _prev_str(data)
if data not in infinity_versions: if data not in infinity_versions:
break break
return VersionStrComponent(data) return VersionStrComponent(data)
def next_version(v: StandardVersion) -> StandardVersion: def _next_version(v: StandardVersion) -> StandardVersion:
if len(v.version) == 0: release, prerelease = v.version
nxt = VersionStrComponent("A") separators = v.separators
elif isinstance(v.version[-1], VersionStrComponent): prerelease_type = prerelease[0]
nxt = next_version_str_component(v.version[-1]) if prerelease_type != FINAL:
prerelease = (prerelease_type, prerelease[1] + 1 if len(prerelease) > 1 else 0)
elif len(release) == 0:
release = (VersionStrComponent("A"),)
separators = ("",)
elif isinstance(release[-1], VersionStrComponent):
release = release[:-1] + (_next_version_str_component(release[-1]),)
else: else:
nxt = v.version[-1] + 1 release = release[:-1] + (release[-1] + 1,)
components = [""] * (2 * len(release))
# Construct a string-version for printing components[::2] = release
string_components = [] components[1::2] = separators[: len(release)]
for part, sep in zip(v.version[:-1], v.separators): if prerelease_type != FINAL:
string_components.append(str(part)) components.extend((PRERELEASE_TO_STRING[prerelease_type], prerelease[1]))
string_components.append(str(sep)) return StandardVersion("".join(str(c) for c in components), (release, prerelease), separators)
string_components.append(str(nxt))
return StandardVersion("".join(string_components), v.version[:-1] + (nxt,), v.separators)
def prev_version(v: StandardVersion) -> StandardVersion: def _prev_version(v: StandardVersion) -> StandardVersion:
if len(v.version) == 0: # this function does not deal with underflow, because it's always called as
# _prev_version(_next_version(v)).
release, prerelease = v.version
separators = v.separators
prerelease_type = prerelease[0]
if prerelease_type != FINAL:
prerelease = (
(prerelease_type,) if prerelease[1] == 0 else (prerelease_type, prerelease[1] - 1)
)
elif len(release) == 0:
return v return v
elif isinstance(v.version[-1], VersionStrComponent): elif isinstance(release[-1], VersionStrComponent):
prev = prev_version_str_component(v.version[-1]) release = release[:-1] + (_prev_version_str_component(release[-1]),)
else: else:
prev = v.version[-1] - 1 release = release[:-1] + (release[-1] - 1,)
components = [""] * (2 * len(release))
# Construct a string-version for printing components[::2] = release
string_components = [] components[1::2] = separators[: len(release)]
for part, sep in zip(v.version[:-1], v.separators): if prerelease_type != FINAL:
string_components.append(str(part)) components.extend((PRERELEASE_TO_STRING[prerelease_type], *prerelease[1:]))
string_components.append(str(sep)) return StandardVersion("".join(str(c) for c in components), (release, prerelease), separators)
string_components.append(str(prev))
return StandardVersion("".join(string_components), v.version[:-1] + (prev,), v.separators)
def Version(string: Union[str, int]) -> Union[GitVersion, StandardVersion]: def Version(string: Union[str, int]) -> Union[GitVersion, StandardVersion]:

View File

@ -76,7 +76,7 @@ class PyAzureCli(PythonPackage):
depends_on("py-azure-mgmt-recoveryservices@0.4.0:0.4", type=("build", "run")) depends_on("py-azure-mgmt-recoveryservices@0.4.0:0.4", type=("build", "run"))
depends_on("py-azure-mgmt-recoveryservicesbackup@0.6.0:0.6", type=("build", "run")) depends_on("py-azure-mgmt-recoveryservicesbackup@0.6.0:0.6", type=("build", "run"))
depends_on("py-azure-mgmt-redhatopenshift@0.1.0", type=("build", "run")) depends_on("py-azure-mgmt-redhatopenshift@0.1.0", type=("build", "run"))
depends_on("py-azure-mgmt-redis@7.0.0:7.0", type=("build", "run")) depends_on("py-azure-mgmt-redis@7.0", type=("build", "run"))
depends_on("py-azure-mgmt-relay@0.1.0:0.1", type=("build", "run")) depends_on("py-azure-mgmt-relay@0.1.0:0.1", type=("build", "run"))
depends_on("py-azure-mgmt-reservations@0.6.0", type=("build", "run")) depends_on("py-azure-mgmt-reservations@0.6.0", type=("build", "run"))
depends_on("py-azure-mgmt-search@2.0:2", type=("build", "run")) depends_on("py-azure-mgmt-search@2.0:2", type=("build", "run"))

View File

@ -28,7 +28,7 @@ class PyGtdbtk(PythonPackage):
depends_on("py-pydantic@1.9.2:1", type=("build", "run"), when="@2.3.0:") depends_on("py-pydantic@1.9.2:1", type=("build", "run"), when="@2.3.0:")
depends_on("prodigal@2.6.2:", type=("build", "run")) depends_on("prodigal@2.6.2:", type=("build", "run"))
depends_on("hmmer@3.1b2:", type=("build", "run")) depends_on("hmmer@3.1b2:", type=("build", "run"))
depends_on("pplacer@1.1:", type=("build", "run")) depends_on("pplacer@1.1alpha:", type=("build", "run"))
depends_on("fastani@1.32:", type=("build", "run")) depends_on("fastani@1.32:", type=("build", "run"))
depends_on("fasttree@2.1.9:", type=("build", "run")) depends_on("fasttree@2.1.9:", type=("build", "run"))
depends_on("mash@2.2:", type=("build", "run")) depends_on("mash@2.2:", type=("build", "run"))