summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarmen Stoppels <me@harmenstoppels.nl>2024-03-22 23:30:32 +0100
committerGitHub <noreply@github.com>2024-03-22 23:30:32 +0100
commitc3eaf4d6cf46e5d9387db851d5dcee113f058710 (patch)
tree4a1491cae5745a47fc5c94eceaa9ce28c40eab3e
parent397334a4befea7d76c5320824a0742259c225475 (diff)
downloadspack-c3eaf4d6cf46e5d9387db851d5dcee113f058710.tar.gz
spack-c3eaf4d6cf46e5d9387db851d5dcee113f058710.tar.bz2
spack-c3eaf4d6cf46e5d9387db851d5dcee113f058710.tar.xz
spack-c3eaf4d6cf46e5d9387db851d5dcee113f058710.zip
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.
-rw-r--r--lib/spack/docs/basic_usage.rst3
-rw-r--r--lib/spack/docs/packaging_guide.rst57
-rw-r--r--lib/spack/spack/solver/asp.py3
-rw-r--r--lib/spack/spack/target.py5
-rw-r--r--lib/spack/spack/test/concretize.py25
-rw-r--r--lib/spack/spack/test/url_fetch.py12
-rw-r--r--lib/spack/spack/test/versions.py83
-rw-r--r--lib/spack/spack/version/__init__.py8
-rw-r--r--lib/spack/spack/version/common.py8
-rw-r--r--lib/spack/spack/version/version_types.py182
-rw-r--r--var/spack/repos/builtin/packages/py-azure-cli/package.py2
-rw-r--r--var/spack/repos/builtin/packages/py-gtdbtk/package.py2
12 files changed, 275 insertions, 115 deletions
diff --git a/lib/spack/docs/basic_usage.rst b/lib/spack/docs/basic_usage.rst
index 5a8d9668f3..f26c6e5683 100644
--- a/lib/spack/docs/basic_usage.rst
+++ b/lib/spack/docs/basic_usage.rst
@@ -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``,
diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst
index 0fa8e4dce3..7d8efa3987 100644
--- a/lib/spack/docs/packaging_guide.rst
+++ b/lib/spack/docs/packaging_guide.rst
@@ -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
^^^^^^^^^^^^^^^^^^
-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.
-
-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 imposes a generic total ordering on the set of versions,
+independently from the package they are associated with.
-Spack versions may also be arbitrary non-numeric strings, for example
-``develop``, ``master``, ``local``.
-
-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:
+Most Spack versions are numeric, a tuple of integers; for example,
+``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``.
+
+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``.
+
+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``.
+
+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
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index c2524b1223..2cf4b5cbf4 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -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 @@ class SpackSolverSetup:
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:
diff --git a/lib/spack/spack/target.py b/lib/spack/spack/target.py
index 5f1f4f9e5a..8f9e6dcf51 100644
--- a/lib/spack/spack/target.py
+++ b/lib/spack/spack/target.py
@@ -155,5 +155,6 @@ class Target:
# 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
+ )
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
index b12fbfb910..a68d0f7d34 100644
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -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
+ ]
diff --git a/lib/spack/spack/test/url_fetch.py b/lib/spack/spack/test/url_fetch.py
index 406344ebfb..6dd548d858 100644
--- a/lib/spack/spack/test/url_fetch.py
+++ b/lib/spack/spack/test/url_fetch.py
@@ -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)
diff --git a/lib/spack/spack/test/versions.py b/lib/spack/spack/test/versions.py
index 48ab551060..bc97fe4667 100644
--- a/lib/spack/spack/test/versions.py
+++ b/lib/spack/spack/test/versions.py
@@ -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 test_repr_and_str():
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
diff --git a/lib/spack/spack/version/__init__.py b/lib/spack/spack/version/__init__.py
index 30dd71ea62..18d739ae0c 100644
--- a/lib/spack/spack/version/__init__.py
+++ b/lib/spack/spack/version/__init__.py
@@ -30,9 +30,9 @@ from .version_types import (
Version,
VersionList,
VersionRange,
+ _next_version,
+ _prev_version,
from_string,
- next_version,
- prev_version,
ver,
)
@@ -46,8 +46,8 @@ __all__ = [
"from_string",
"is_git_version",
"infinity_versions",
- "prev_version",
- "next_version",
+ "_prev_version",
+ "_next_version",
"VersionList",
"ClosedOpenRange",
"StandardVersion",
diff --git a/lib/spack/spack/version/common.py b/lib/spack/spack/version/common.py
index 3afa218d4d..89434d3930 100644
--- a/lib/spack/spack/version/common.py
+++ b/lib/spack/spack/version/common.py
@@ -15,6 +15,14 @@ infinity_versions = ["stable", "trunk", "head", "master", "main", "develop"]
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 (
diff --git a/lib/spack/spack/version/version_types.py b/lib/spack/spack/version/version_types.py
index 3e403256ea..b772cd9d90 100644
--- a/lib/spack/spack/version/version_types.py
+++ b/lib/spack/spack/version/version_types.py
@@ -11,7 +11,11 @@ from typing import List, Optional, Tuple, Union
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 @@ class StandardVersion(ConcreteVersion):
@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 @@ class StandardVersion(ConcreteVersion):
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 @@ class StandardVersion(ConcreteVersion):
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 @@ class StandardVersion(ConcreteVersion):
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 @@ class GitVersion(ConcreteVersion):
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 @@ class ClosedOpenRange:
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 @@ class ClosedOpenRange:
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 @@ class VersionList:
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 @@ class VersionList:
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)
-
-
-def prev_version(v: StandardVersion) -> StandardVersion:
- if len(v.version) == 0:
+ 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:
+ # 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]:
diff --git a/var/spack/repos/builtin/packages/py-azure-cli/package.py b/var/spack/repos/builtin/packages/py-azure-cli/package.py
index 43f8f546a8..27cb5a34c5 100644
--- a/var/spack/repos/builtin/packages/py-azure-cli/package.py
+++ b/var/spack/repos/builtin/packages/py-azure-cli/package.py
@@ -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"))
diff --git a/var/spack/repos/builtin/packages/py-gtdbtk/package.py b/var/spack/repos/builtin/packages/py-gtdbtk/package.py
index a78e86eed0..08eda2bc7d 100644
--- a/var/spack/repos/builtin/packages/py-gtdbtk/package.py
+++ b/var/spack/repos/builtin/packages/py-gtdbtk/package.py
@@ -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"))