diff options
author | Harmen Stoppels <me@harmenstoppels.nl> | 2024-03-22 23:30:32 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-22 23:30:32 +0100 |
commit | c3eaf4d6cf46e5d9387db851d5dcee113f058710 (patch) | |
tree | 4a1491cae5745a47fc5c94eceaa9ce28c40eab3e | |
parent | 397334a4befea7d76c5320824a0742259c225475 (diff) | |
download | spack-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.rst | 3 | ||||
-rw-r--r-- | lib/spack/docs/packaging_guide.rst | 57 | ||||
-rw-r--r-- | lib/spack/spack/solver/asp.py | 3 | ||||
-rw-r--r-- | lib/spack/spack/target.py | 5 | ||||
-rw-r--r-- | lib/spack/spack/test/concretize.py | 25 | ||||
-rw-r--r-- | lib/spack/spack/test/url_fetch.py | 12 | ||||
-rw-r--r-- | lib/spack/spack/test/versions.py | 83 | ||||
-rw-r--r-- | lib/spack/spack/version/__init__.py | 8 | ||||
-rw-r--r-- | lib/spack/spack/version/common.py | 8 | ||||
-rw-r--r-- | lib/spack/spack/version/version_types.py | 182 | ||||
-rw-r--r-- | var/spack/repos/builtin/packages/py-azure-cli/package.py | 2 | ||||
-rw-r--r-- | var/spack/repos/builtin/packages/py-gtdbtk/package.py | 2 |
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")) |