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
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
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``,

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
``v6-16-00-patches``, or a URL specifying a regularly updated snapshot tarball.
.. _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,
``0.1``, ``6.96`` or ``1.2.3.1``. Spack knows how to compare and sort
numeric versions.
``0.1``, ``6.96`` or ``1.2.3.1``. In this very basic case, version
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
example, ``py-sphinx-rtd-theme@=0.1.10a0``. In this case, numbers are
always considered to be "newer" than letters. This is for consistency
with `RPM <https://bugzilla.redhat.com/show_bug.cgi?id=50977>`_.
Spack can also supports string components such as ``1.1.1a`` and
``1.y.0``. String components are considered less than numeric
components, so ``1.y.0 < 1.0``. This is for consistency with
`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
``develop``, ``master``, ``local``.
Pre-release suffixes also contain string parts, but they are handled
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
into a list of components based on delimiters such as ``.``, ``-`` etc.
Lists are then ordered lexicographically, where components are ordered
as follows:
Finally, there are a few special string components that are considered
"infinity versions". They include ``develop``, ``main``, ``master``,
``head``, ``trunk``, and ``stable``. For example: ``1.2 < develop``.
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
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,
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:
#. 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),
not info.get("deprecated", False),
not version.isdevelop(),
not version.is_prerelease(),
version,
)
@ -2158,7 +2159,7 @@ def versions_for(v):
if isinstance(v, vn.StandardVersion):
return [v]
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):
return sum((versions_for(e) for e in v), [])
else:

View File

@ -155,5 +155,6 @@ def optimization_flags(self, compiler):
# log this and just return compiler.version instead
tty.debug(str(e))
compiler_version = compiler_version.dotted.force_numeric
return self.microarchitecture.optimization_flags(compiler.name, str(compiler_version))
return self.microarchitecture.optimization_flags(
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}]}},
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(
"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.
("=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
# 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"),
("=2.0.0", "foo-2.0.0.tar.gz", None),
],
)
@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."""
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)
assert isinstance(fetch_strategy, fs.URLFetchStrategy)

View File

@ -213,12 +213,24 @@ def test_nums_and_patch():
assert_ver_gt("=6.5p1", "=5.6p1")
def test_rc_versions():
assert_ver_gt("=6.0.rc1", "=6.0")
assert_ver_lt("=6.0", "=6.0.rc1")
def test_prereleases():
# pre-releases are special: they are less than final releases
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():
# these are not pre-releases, but ordinary string components.
assert_ver_gt("=10b2", "=10a1")
assert_ver_lt("=10a2", "=10b2")
@ -277,6 +289,39 @@ def test_version_ranges():
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():
assert_in("=1.3", "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")
assert_satisfies("4.7.3b2", "4.7")
assert_satisfies("4.7b6", "4.7")
assert_satisfies("4.7.3v2", "4.7")
assert_satisfies("4.7v6", "4.7")
assert_satisfies("4.7.3", "4")
assert_satisfies("4.7.3b2", "4")
assert_satisfies("4.7b6", "4")
assert_satisfies("4.7.3v2", "4")
assert_satisfies("4.7v6", "4")
assert_does_not_satisfy("4.8.0", "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"])
assert_satisfies(["4.7.3b2"], ["4.7"])
assert_satisfies(["4.7b6"], ["4.7"])
assert_satisfies(["4.7.3v2"], ["4.7"])
assert_satisfies(["4.7v6"], ["4.7"])
assert_satisfies(["4.7.3"], ["4"])
assert_satisfies(["4.7.3b2"], ["4"])
assert_satisfies(["4.7b6"], ["4"])
assert_satisfies(["4.7.3v2"], ["4"])
assert_satisfies(["4.7v6"], ["4"])
assert_does_not_satisfy(["4.8.0"], ["4.9"])
assert_does_not_satisfy(["4.8"], ["4.9"])
@ -507,6 +552,11 @@ def test_formatted_strings():
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():
v = Version("1.23-4_5b")
@ -548,9 +598,18 @@ def check_repr_and_str(vrs):
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():
a = Version("1.2.3.4")
assert len(a) == len(a.version)
assert len(a) == len(a.version[0])
assert len(a) == 4
b = Version("2018.0")
assert len(b) == 2

View File

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

View File

@ -15,6 +15,14 @@
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:
return (

View File

@ -11,7 +11,11 @@
from spack.util.spack_yaml import syaml_dict
from .common import (
ALPHA,
COMMIT_VERSION,
FINAL,
PRERELEASE_TO_STRING,
STRING_TO_PRERELEASE,
EmptyRangeError,
VersionLookupError,
infinity_versions,
@ -88,21 +92,50 @@ def parse_string_components(string: str) -> Tuple[tuple, tuple]:
raise ValueError("Bad characters in version string: %s" % 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)
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:
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 to represent versions"""
__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.version = version
self.separators = separators
@ -113,11 +146,13 @@ def from_string(string: str):
@staticmethod
def typemin():
return StandardVersion("", (), ())
return StandardVersion("", ((), (ALPHA,)), ("",))
@staticmethod
def typemax():
return StandardVersion("infinity", (VersionStrComponent(len(infinity_versions)),), ())
return StandardVersion(
"infinity", ((VersionStrComponent(len(infinity_versions)),), (FINAL,)), ("",)
)
def __bool__(self):
return True
@ -164,21 +199,23 @@ def __gt__(self, other):
return NotImplemented
def __iter__(self):
return iter(self.version)
return iter(self.version[0])
def __len__(self):
return len(self.version)
return len(self.version[0])
def __getitem__(self, idx):
cls = type(self)
release = self.version[0]
if isinstance(idx, numbers.Integral):
return self.version[idx]
return release[idx]
elif isinstance(idx, slice):
string_arg = []
pairs = zip(self.version[idx], self.separators[idx])
pairs = zip(release[idx], self.separators[idx])
for token, sep in pairs:
string_arg.append(str(token))
string_arg.append(str(sep))
@ -193,22 +230,16 @@ def __getitem__(self, idx):
message = "{cls.__name__} indices must be integers"
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):
return self.string or self._stringify()
return self.string or _stringify_version(self.version, self.separators)
def __repr__(self) -> str:
# Print indirect repr through Version(...)
return f'Version("{str(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):
# 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):
"""Triggers on the special case of the `@develop-like` version."""
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
def force_numeric(self):
"""Replaces all non-numeric components of the version with 0
def dotted_numeric_string(self) -> str:
"""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.
"""
numeric = tuple(0 if isinstance(v, VersionStrComponent) else v for v in self.version)
# null separators except the final one have to be converted to avoid concatenating ints
# default to '.' as most common delimiter for versions
separators = tuple(
"." if s == "" and i != len(self.separators) - 1 else s
for i, s in enumerate(self.separators)
)
return type(self)(None, numeric, separators)
numeric = tuple(0 if isinstance(v, VersionStrComponent) else v for v in self.version[0])
if self.is_prerelease():
numeric += (0, *self.version[1][1:])
return ".".join(str(v) for v in numeric)
@property
def dotted(self):
@ -591,6 +621,9 @@ def __getitem__(self, idx):
def isdevelop(self):
return self.ref_version.isdevelop()
def is_prerelease(self) -> bool:
return self.ref_version.is_prerelease()
@property
def dotted(self) -> StandardVersion:
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):
"""Construct ClosedOpenRange from lo:hi range."""
try:
return ClosedOpenRange(lo, next_version(hi))
return ClosedOpenRange(lo, _next_version(hi))
except EmptyRangeError as e:
raise EmptyRangeError(f"{lo}:{hi} is an empty range") from e
def __str__(self):
# This simplifies 3.1:<3.2 to 3.1:3.1 to 3.1
# 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:
return str(self.lo)
lhs = "" if self.lo == StandardVersion.typemin() else str(self.lo)
@ -641,7 +674,7 @@ def __repr__(self):
def __hash__(self):
# 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):
if isinstance(other, StandardVersion):
@ -823,7 +856,7 @@ def concrete_range_as_version(self) -> Optional[ConcreteVersion]:
v = self[0]
if isinstance(v, ConcreteVersion):
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 None
@ -994,7 +1027,7 @@ def __repr__(self):
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"""
return (
(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"""
return (
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
masteq -> mastes
@ -1025,14 +1058,14 @@ def next_version_str_component(v: VersionStrComponent) -> VersionStrComponent:
# Find the next non-infinity string.
while True:
data = next_str(data)
data = _next_str(data)
if data not in infinity_versions:
break
return VersionStrComponent(data)
def prev_version_str_component(v: VersionStrComponent) -> VersionStrComponent:
def _prev_version_str_component(v: VersionStrComponent) -> VersionStrComponent:
"""
Produce the previous VersionStrComponent, where
mastes -> masteq
@ -1045,47 +1078,56 @@ def prev_version_str_component(v: VersionStrComponent) -> VersionStrComponent:
# Find the next string.
while True:
data = prev_str(data)
data = _prev_str(data)
if data not in infinity_versions:
break
return VersionStrComponent(data)
def next_version(v: StandardVersion) -> StandardVersion:
if len(v.version) == 0:
nxt = VersionStrComponent("A")
elif isinstance(v.version[-1], VersionStrComponent):
nxt = next_version_str_component(v.version[-1])
def _next_version(v: StandardVersion) -> StandardVersion:
release, prerelease = v.version
separators = v.separators
prerelease_type = prerelease[0]
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:
nxt = v.version[-1] + 1
# Construct a string-version for printing
string_components = []
for part, sep in zip(v.version[:-1], v.separators):
string_components.append(str(part))
string_components.append(str(sep))
string_components.append(str(nxt))
return StandardVersion("".join(string_components), v.version[:-1] + (nxt,), v.separators)
release = release[:-1] + (release[-1] + 1,)
components = [""] * (2 * len(release))
components[::2] = release
components[1::2] = separators[: len(release)]
if prerelease_type != FINAL:
components.extend((PRERELEASE_TO_STRING[prerelease_type], prerelease[1]))
return StandardVersion("".join(str(c) for c in components), (release, prerelease), separators)
def prev_version(v: StandardVersion) -> StandardVersion:
if len(v.version) == 0:
def _prev_version(v: StandardVersion) -> StandardVersion:
# 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
elif isinstance(v.version[-1], VersionStrComponent):
prev = prev_version_str_component(v.version[-1])
elif isinstance(release[-1], VersionStrComponent):
release = release[:-1] + (_prev_version_str_component(release[-1]),)
else:
prev = v.version[-1] - 1
# Construct a string-version for printing
string_components = []
for part, sep in zip(v.version[:-1], v.separators):
string_components.append(str(part))
string_components.append(str(sep))
string_components.append(str(prev))
return StandardVersion("".join(string_components), v.version[:-1] + (prev,), v.separators)
release = release[:-1] + (release[-1] - 1,)
components = [""] * (2 * len(release))
components[::2] = release
components[1::2] = separators[: len(release)]
if prerelease_type != FINAL:
components.extend((PRERELEASE_TO_STRING[prerelease_type], *prerelease[1:]))
return StandardVersion("".join(str(c) for c in components), (release, prerelease), separators)
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-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-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-reservations@0.6.0", 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("prodigal@2.6.2:", 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("fasttree@2.1.9:", type=("build", "run"))
depends_on("mash@2.2:", type=("build", "run"))