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:
parent
397334a4be
commit
c3eaf4d6cf
@ -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``,
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
]
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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 (
|
||||||
|
@ -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]:
|
||||||
|
@ -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"))
|
||||||
|
@ -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"))
|
||||||
|
Loading…
Reference in New Issue
Block a user