version_types
: clean up type hierarchy and add annotations (#47781)
In preparation for adding `when=` to `version()`, I'm cleaning up the types in `version_types` and making sure the methods here pass `mypy` checks. This started as an attempt to use `ConcreteVersion` outside of `spack.version` and grew into a larger type refactor. The hierarchy now looks like this: * `VersionType` * `ConcreteVersion` * `StandardVersion` * `GitVersion` * `ClosedOpenRange` * `VersionList` Note that the top-level thing can't easily be `Version` as that is a method and it returns only `ConcreteVersion` right now. I *could* do something fancy with `__new__` to make `Version` a synonym for the `ConcreteVersion` constructor, which would allow it to be used as a type. I could also do something similar with `VersionRange` but not sure if it's worth it just to make these into types. There are still some places where I think `GitVersion` might not be handled properly, but I have not attempted to fix those here. - [x] Add a top-level `VersionType` class that all version types extend from - [x] Define and document common methods and rich comparisons on `VersionType` - [x] Replace complicated `Union` types with `VersionType` and `ConcreteVersion` as needed - [x] Annotate most methods (skipping `__getitem__` and friends as the typing is a pain) - [x] Fix up the `VersionList` constructor a bit - [x] Add cases to methods that weren't handling all `VersionType`s - [x] Rework some places to clarify typing for `mypy` - [x] Simplify / optimize _next_version - [x] Make StandardVersion.string a property to enable lazy comparison Signed-off-by: Todd Gamblin <tgamblin@llnl.gov>
This commit is contained in:
parent
c6e292f55f
commit
2a2d1989c1
@ -607,6 +607,9 @@ def test_stringify_version(version_str):
|
|||||||
v.string = None
|
v.string = None
|
||||||
assert str(v) == version_str
|
assert str(v) == version_str
|
||||||
|
|
||||||
|
v.string = None
|
||||||
|
assert v.string == version_str
|
||||||
|
|
||||||
|
|
||||||
def test_len():
|
def test_len():
|
||||||
a = Version("1.2.3.4")
|
a = Version("1.2.3.4")
|
||||||
|
@ -25,11 +25,13 @@
|
|||||||
)
|
)
|
||||||
from .version_types import (
|
from .version_types import (
|
||||||
ClosedOpenRange,
|
ClosedOpenRange,
|
||||||
|
ConcreteVersion,
|
||||||
GitVersion,
|
GitVersion,
|
||||||
StandardVersion,
|
StandardVersion,
|
||||||
Version,
|
Version,
|
||||||
VersionList,
|
VersionList,
|
||||||
VersionRange,
|
VersionRange,
|
||||||
|
VersionType,
|
||||||
_next_version,
|
_next_version,
|
||||||
_prev_version,
|
_prev_version,
|
||||||
from_string,
|
from_string,
|
||||||
@ -40,21 +42,23 @@
|
|||||||
any_version: VersionList = VersionList([":"])
|
any_version: VersionList = VersionList([":"])
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Version",
|
|
||||||
"VersionRange",
|
|
||||||
"ver",
|
|
||||||
"from_string",
|
|
||||||
"is_git_version",
|
|
||||||
"infinity_versions",
|
|
||||||
"_prev_version",
|
|
||||||
"_next_version",
|
|
||||||
"VersionList",
|
|
||||||
"ClosedOpenRange",
|
"ClosedOpenRange",
|
||||||
"StandardVersion",
|
"ConcreteVersion",
|
||||||
"GitVersion",
|
|
||||||
"VersionError",
|
|
||||||
"VersionChecksumError",
|
|
||||||
"VersionLookupError",
|
|
||||||
"EmptyRangeError",
|
"EmptyRangeError",
|
||||||
|
"GitVersion",
|
||||||
|
"StandardVersion",
|
||||||
|
"Version",
|
||||||
|
"VersionChecksumError",
|
||||||
|
"VersionError",
|
||||||
|
"VersionList",
|
||||||
|
"VersionLookupError",
|
||||||
|
"VersionRange",
|
||||||
|
"VersionType",
|
||||||
|
"_next_version",
|
||||||
|
"_prev_version",
|
||||||
"any_version",
|
"any_version",
|
||||||
|
"from_string",
|
||||||
|
"infinity_versions",
|
||||||
|
"is_git_version",
|
||||||
|
"ver",
|
||||||
]
|
]
|
||||||
|
@ -3,10 +3,9 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||||
|
|
||||||
import numbers
|
|
||||||
import re
|
import re
|
||||||
from bisect import bisect_left
|
from bisect import bisect_left
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from spack.util.spack_yaml import syaml_dict
|
from spack.util.spack_yaml import syaml_dict
|
||||||
|
|
||||||
@ -32,26 +31,44 @@
|
|||||||
|
|
||||||
|
|
||||||
class VersionStrComponent:
|
class VersionStrComponent:
|
||||||
|
"""Internal representation of the string (non-integer) components of Spack versions.
|
||||||
|
|
||||||
|
Versions comprise string and integer components (see ``SEGMENT_REGEX`` above).
|
||||||
|
|
||||||
|
This represents a string component, which is either some component consisting only
|
||||||
|
of alphabetical characters, *or* a special "infinity version" like ``main``,
|
||||||
|
``develop``, ``master``, etc.
|
||||||
|
|
||||||
|
For speed, Spack versions are designed to map to Python tuples, so that we can use
|
||||||
|
Python's fast lexicographic tuple comparison on them. ``VersionStrComponent`` is
|
||||||
|
designed to work as a component in these version tuples, and as such must compare
|
||||||
|
directly with ``int`` or other ``VersionStrComponent`` objects.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
__slots__ = ["data"]
|
__slots__ = ["data"]
|
||||||
|
|
||||||
def __init__(self, data):
|
data: Union[int, str]
|
||||||
|
|
||||||
|
def __init__(self, data: Union[int, str]):
|
||||||
# int for infinity index, str for literal.
|
# int for infinity index, str for literal.
|
||||||
self.data: Union[int, str] = data
|
self.data = data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(string):
|
def from_string(string: str) -> "VersionStrComponent":
|
||||||
|
value: Union[int, str] = string
|
||||||
if len(string) >= iv_min_len:
|
if len(string) >= iv_min_len:
|
||||||
try:
|
try:
|
||||||
string = infinity_versions.index(string)
|
value = infinity_versions.index(string)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return VersionStrComponent(string)
|
return VersionStrComponent(value)
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return hash(self.data)
|
return hash(self.data)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return (
|
return (
|
||||||
("infinity" if self.data >= len(infinity_versions) else infinity_versions[self.data])
|
("infinity" if self.data >= len(infinity_versions) else infinity_versions[self.data])
|
||||||
if isinstance(self.data, int)
|
if isinstance(self.data, int)
|
||||||
@ -61,38 +78,61 @@ def __str__(self):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'VersionStrComponent("{self}")'
|
return f'VersionStrComponent("{self}")'
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: object) -> bool:
|
||||||
return isinstance(other, VersionStrComponent) and self.data == other.data
|
return isinstance(other, VersionStrComponent) and self.data == other.data
|
||||||
|
|
||||||
def __lt__(self, other):
|
# ignore typing for certain parts of these methods b/c a) they are performance-critical, and
|
||||||
lhs_inf = isinstance(self.data, int)
|
# b) mypy isn't smart enough to figure out that if l_inf and r_inf are the same, comparing
|
||||||
|
# self.data and other.data is type safe.
|
||||||
|
def __lt__(self, other: object) -> bool:
|
||||||
|
l_inf = isinstance(self.data, int)
|
||||||
if isinstance(other, int):
|
if isinstance(other, int):
|
||||||
return not lhs_inf
|
return not l_inf
|
||||||
rhs_inf = isinstance(other.data, int)
|
r_inf = isinstance(other.data, int) # type: ignore
|
||||||
return (not lhs_inf and rhs_inf) if lhs_inf ^ rhs_inf else self.data < other.data
|
return (not l_inf and r_inf) if l_inf ^ r_inf else self.data < other.data # type: ignore
|
||||||
|
|
||||||
def __le__(self, other):
|
def __gt__(self, other: object) -> bool:
|
||||||
|
l_inf = isinstance(self.data, int)
|
||||||
|
if isinstance(other, int):
|
||||||
|
return l_inf
|
||||||
|
r_inf = isinstance(other.data, int) # type: ignore
|
||||||
|
return (l_inf and not r_inf) if l_inf ^ r_inf else self.data > other.data # type: ignore
|
||||||
|
|
||||||
|
def __le__(self, other: object) -> bool:
|
||||||
return self < other or self == other
|
return self < other or self == other
|
||||||
|
|
||||||
def __gt__(self, other):
|
def __ge__(self, other: object) -> bool:
|
||||||
lhs_inf = isinstance(self.data, int)
|
|
||||||
if isinstance(other, int):
|
|
||||||
return lhs_inf
|
|
||||||
rhs_inf = isinstance(other.data, int)
|
|
||||||
return (lhs_inf and not rhs_inf) if lhs_inf ^ rhs_inf else self.data > other.data
|
|
||||||
|
|
||||||
def __ge__(self, other):
|
|
||||||
return self > other or self == other
|
return self > other or self == other
|
||||||
|
|
||||||
|
|
||||||
def parse_string_components(string: str) -> Tuple[tuple, tuple]:
|
# Tuple types that make up the internal representation of StandardVersion.
|
||||||
|
# We use Tuples so that Python can quickly compare versions.
|
||||||
|
|
||||||
|
#: Version components are integers for numeric parts, VersionStrComponents for string parts.
|
||||||
|
VersionComponentTuple = Tuple[Union[int, VersionStrComponent], ...]
|
||||||
|
|
||||||
|
#: A Prerelease identifier is a constant for alpha/beta/rc/final and one optional number.
|
||||||
|
#: Most versions will have this set to ``(FINAL,)``. Prereleases will have some other
|
||||||
|
#: initial constant followed by a number, e.g. ``(RC, 1)``.
|
||||||
|
PrereleaseTuple = Tuple[int, ...]
|
||||||
|
|
||||||
|
#: Actual version tuple, including the split version number itself and the prerelease,
|
||||||
|
#: all represented as tuples.
|
||||||
|
VersionTuple = Tuple[VersionComponentTuple, PrereleaseTuple]
|
||||||
|
|
||||||
|
#: Separators from a parsed version.
|
||||||
|
SeparatorTuple = Tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_string_components(string: str) -> Tuple[VersionTuple, SeparatorTuple]:
|
||||||
|
"""Parse a string into a ``VersionTuple`` and ``SeparatorTuple``."""
|
||||||
string = string.strip()
|
string = string.strip()
|
||||||
|
|
||||||
if string and not VALID_VERSION.match(string):
|
if string and not VALID_VERSION.match(string):
|
||||||
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)
|
||||||
separators = tuple(m[2] for m in segments)
|
separators: Tuple[str] = tuple(m[2] for m in segments)
|
||||||
prerelease: Tuple[int, ...]
|
prerelease: Tuple[int, ...]
|
||||||
|
|
||||||
# <version>(alpha|beta|rc)<number>
|
# <version>(alpha|beta|rc)<number>
|
||||||
@ -109,63 +149,150 @@ def parse_string_components(string: str) -> Tuple[tuple, tuple]:
|
|||||||
else:
|
else:
|
||||||
prerelease = (FINAL,)
|
prerelease = (FINAL,)
|
||||||
|
|
||||||
release = tuple(int(m[0]) if m[0] else VersionStrComponent.from_string(m[1]) for m in segments)
|
release: VersionComponentTuple = tuple(
|
||||||
|
int(m[0]) if m[0] else VersionStrComponent.from_string(m[1]) for m in segments
|
||||||
|
)
|
||||||
|
|
||||||
return (release, prerelease), separators
|
return (release, prerelease), separators
|
||||||
|
|
||||||
|
|
||||||
class ConcreteVersion:
|
class VersionType:
|
||||||
pass
|
"""Base type for all versions in Spack (ranges, lists, regular versions, and git versions).
|
||||||
|
|
||||||
|
Versions in Spack behave like sets, and support some basic set operations. There are
|
||||||
|
four subclasses of ``VersionType``:
|
||||||
|
|
||||||
|
* ``StandardVersion``: a single, concrete version, e.g. 3.4.5 or 5.4b0.
|
||||||
|
* ``GitVersion``: subclass of ``StandardVersion`` for handling git repositories.
|
||||||
|
* ``ClosedOpenRange``: an inclusive version range, closed or open, e.g. ``3.0:5.0``,
|
||||||
|
``3.0:``, or ``:5.0``
|
||||||
|
* ``VersionList``: An ordered list of any of the above types.
|
||||||
|
|
||||||
|
Notably, when Spack parses a version, it's always a range *unless* specified with
|
||||||
|
``@=`` to make it concrete.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def intersection(self, other: "VersionType") -> "VersionType":
|
||||||
|
"""Any versions contained in both self and other, or empty VersionList if no overlap."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def intersects(self, other: "VersionType") -> bool:
|
||||||
|
"""Whether self and other overlap."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def overlaps(self, other: "VersionType") -> bool:
|
||||||
|
"""Whether self and other overlap (same as ``intersects()``)."""
|
||||||
|
return self.intersects(other)
|
||||||
|
|
||||||
|
def satisfies(self, other: "VersionType") -> bool:
|
||||||
|
"""Whether self is entirely contained in other."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def union(self, other: "VersionType") -> "VersionType":
|
||||||
|
"""Return a VersionType containing self and other."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# We can use SupportsRichComparisonT in Python 3.8 or later, but alas in 3.6 we need
|
||||||
|
# to write all the operators out
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __lt__(self, other: object) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __gt__(self, other: object) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __ge__(self, other: object) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __le__(self, other: object) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
def _stringify_version(versions: Tuple[tuple, tuple], separators: tuple) -> str:
|
class ConcreteVersion(VersionType):
|
||||||
|
"""Base type for versions that represents a single (non-range or list) version."""
|
||||||
|
|
||||||
|
|
||||||
|
def _stringify_version(versions: VersionTuple, separators: Tuple[str, ...]) -> str:
|
||||||
|
"""Create a string representation from version components."""
|
||||||
release, prerelease = versions
|
release, prerelease = versions
|
||||||
string = ""
|
|
||||||
for i in range(len(release)):
|
components = [f"{rel}{sep}" for rel, sep in zip(release, separators)]
|
||||||
string += f"{release[i]}{separators[i]}"
|
|
||||||
if prerelease[0] != FINAL:
|
if prerelease[0] != FINAL:
|
||||||
string += f"{PRERELEASE_TO_STRING[prerelease[0]]}{separators[len(release)]}"
|
components.append(PRERELEASE_TO_STRING[prerelease[0]])
|
||||||
if len(prerelease) > 1:
|
if len(prerelease) > 1:
|
||||||
string += str(prerelease[1])
|
components.append(separators[len(release)])
|
||||||
return string
|
components.append(str(prerelease[1]))
|
||||||
|
|
||||||
|
return "".join(components)
|
||||||
|
|
||||||
|
|
||||||
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[tuple, tuple], separators: tuple):
|
_string: str
|
||||||
self.string = string
|
version: VersionTuple
|
||||||
|
separators: Tuple[str, ...]
|
||||||
|
|
||||||
|
def __init__(self, string: str, version: VersionTuple, separators: Tuple[str, ...]):
|
||||||
|
"""Create a StandardVersion from a string and parsed version components.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
string: The original version string, or ``""`` if the it is not available.
|
||||||
|
version: A tuple as returned by ``parse_string_components()``. Contains two tuples:
|
||||||
|
one with alpha or numeric components and another with prerelease components.
|
||||||
|
separators: separators parsed from the original version string.
|
||||||
|
|
||||||
|
If constructed with ``string=""``, the string will be lazily constructed from components
|
||||||
|
when ``str()`` is called.
|
||||||
|
"""
|
||||||
|
self._string = string
|
||||||
self.version = version
|
self.version = version
|
||||||
self.separators = separators
|
self.separators = separators
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(string: str):
|
def from_string(string: str) -> "StandardVersion":
|
||||||
return StandardVersion(string, *parse_string_components(string))
|
return StandardVersion(string, *parse_string_components(string))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def typemin():
|
def typemin() -> "StandardVersion":
|
||||||
return _STANDARD_VERSION_TYPEMIN
|
return _STANDARD_VERSION_TYPEMIN
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def typemax():
|
def typemax() -> "StandardVersion":
|
||||||
return _STANDARD_VERSION_TYPEMAX
|
return _STANDARD_VERSION_TYPEMAX
|
||||||
|
|
||||||
def __bool__(self):
|
@property
|
||||||
|
def string(self) -> str:
|
||||||
|
if not self._string:
|
||||||
|
self._string = _stringify_version(self.version, self.separators)
|
||||||
|
return self._string
|
||||||
|
|
||||||
|
@string.setter
|
||||||
|
def string(self, string) -> None:
|
||||||
|
self._string = string
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: object) -> bool:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self.version == other.version
|
return self.version == other.version
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other: object) -> bool:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self.version != other.version
|
return self.version != other.version
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other: object) -> bool:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self.version < other.version
|
return self.version < other.version
|
||||||
if isinstance(other, ClosedOpenRange):
|
if isinstance(other, ClosedOpenRange):
|
||||||
@ -173,7 +300,7 @@ def __lt__(self, other):
|
|||||||
return self <= other.lo
|
return self <= other.lo
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __le__(self, other):
|
def __le__(self, other: object) -> bool:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self.version <= other.version
|
return self.version <= other.version
|
||||||
if isinstance(other, ClosedOpenRange):
|
if isinstance(other, ClosedOpenRange):
|
||||||
@ -181,7 +308,7 @@ def __le__(self, other):
|
|||||||
return self <= other.lo
|
return self <= other.lo
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __ge__(self, other):
|
def __ge__(self, other: object) -> bool:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self.version >= other.version
|
return self.version >= other.version
|
||||||
if isinstance(other, ClosedOpenRange):
|
if isinstance(other, ClosedOpenRange):
|
||||||
@ -189,25 +316,25 @@ def __ge__(self, other):
|
|||||||
return self > other.lo
|
return self > other.lo
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __gt__(self, other):
|
def __gt__(self, other: object) -> bool:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self.version > other.version
|
return self.version > other.version
|
||||||
if isinstance(other, ClosedOpenRange):
|
if isinstance(other, ClosedOpenRange):
|
||||||
return self > other.lo
|
return self > other.lo
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self) -> Iterator:
|
||||||
return iter(self.version[0])
|
return iter(self.version[0])
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
return len(self.version[0])
|
return len(self.version[0])
|
||||||
|
|
||||||
def __getitem__(self, idx):
|
def __getitem__(self, idx: Union[int, slice]):
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
|
|
||||||
release = self.version[0]
|
release = self.version[0]
|
||||||
|
|
||||||
if isinstance(idx, numbers.Integral):
|
if isinstance(idx, int):
|
||||||
return release[idx]
|
return release[idx]
|
||||||
|
|
||||||
elif isinstance(idx, slice):
|
elif isinstance(idx, slice):
|
||||||
@ -220,45 +347,38 @@ def __getitem__(self, idx):
|
|||||||
|
|
||||||
if string_arg:
|
if string_arg:
|
||||||
string_arg.pop() # We don't need the last separator
|
string_arg.pop() # We don't need the last separator
|
||||||
string_arg = "".join(string_arg)
|
return cls.from_string("".join(string_arg))
|
||||||
return cls.from_string(string_arg)
|
|
||||||
else:
|
else:
|
||||||
return StandardVersion.from_string("")
|
return StandardVersion.from_string("")
|
||||||
|
|
||||||
message = "{cls.__name__} indices must be integers"
|
raise TypeError(f"{cls.__name__} indices must be integers or slices")
|
||||||
raise TypeError(message.format(cls=cls))
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.string or _stringify_version(self.version, self.separators)
|
return self.string
|
||||||
|
|
||||||
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) -> int:
|
||||||
# If this is a final release, do not hash the prerelease part for backward compat.
|
# 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])
|
return hash(self.version if self.is_prerelease() else self.version[0])
|
||||||
|
|
||||||
def __contains__(rhs, lhs):
|
def __contains__(rhs, lhs) -> bool:
|
||||||
# We should probably get rid of `x in y` for versions, since
|
# We should probably get rid of `x in y` for versions, since
|
||||||
# versions still have a dual interpretation as singleton sets
|
# versions still have a dual interpretation as singleton sets
|
||||||
# or elements. x in y should be: is the lhs-element in the
|
# or elements. x in y should be: is the lhs-element in the
|
||||||
# rhs-set. Instead this function also does subset checks.
|
# rhs-set. Instead this function also does subset checks.
|
||||||
if isinstance(lhs, (StandardVersion, ClosedOpenRange, VersionList)):
|
if isinstance(lhs, VersionType):
|
||||||
return lhs.satisfies(rhs)
|
return lhs.satisfies(rhs)
|
||||||
raise ValueError(lhs)
|
raise TypeError(f"'in' not supported for instances of {type(lhs)}")
|
||||||
|
|
||||||
def intersects(self, other: Union["StandardVersion", "GitVersion", "ClosedOpenRange"]) -> bool:
|
def intersects(self, other: VersionType) -> bool:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self == other
|
return self == other
|
||||||
return other.intersects(self)
|
return other.intersects(self)
|
||||||
|
|
||||||
def overlaps(self, other) -> bool:
|
def satisfies(self, other: VersionType) -> bool:
|
||||||
return self.intersects(other)
|
|
||||||
|
|
||||||
def satisfies(
|
|
||||||
self, other: Union["ClosedOpenRange", "StandardVersion", "GitVersion", "VersionList"]
|
|
||||||
) -> bool:
|
|
||||||
if isinstance(other, GitVersion):
|
if isinstance(other, GitVersion):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -271,19 +391,19 @@ def satisfies(
|
|||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return other.intersects(self)
|
return other.intersects(self)
|
||||||
|
|
||||||
return NotImplemented
|
raise NotImplementedError
|
||||||
|
|
||||||
def union(self, other: Union["ClosedOpenRange", "StandardVersion"]):
|
def union(self, other: VersionType) -> VersionType:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self if self == other else VersionList([self, other])
|
return self if self == other else VersionList([self, other])
|
||||||
return other.union(self)
|
return other.union(self)
|
||||||
|
|
||||||
def intersection(self, other: Union["ClosedOpenRange", "StandardVersion"]):
|
def intersection(self, other: VersionType) -> VersionType:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self if self == other else VersionList()
|
return self if self == other else VersionList()
|
||||||
return other.intersection(self)
|
return other.intersection(self)
|
||||||
|
|
||||||
def isdevelop(self):
|
def isdevelop(self) -> bool:
|
||||||
"""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[0]
|
isinstance(p, VersionStrComponent) and isinstance(p.data, int) for p in self.version[0]
|
||||||
@ -304,7 +424,7 @@ def dotted_numeric_string(self) -> str:
|
|||||||
return ".".join(str(v) for v in numeric)
|
return ".".join(str(v) for v in numeric)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dotted(self):
|
def dotted(self) -> "StandardVersion":
|
||||||
"""The dotted representation of the version.
|
"""The dotted representation of the version.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@ -318,7 +438,7 @@ def dotted(self):
|
|||||||
return type(self).from_string(self.string.replace("-", ".").replace("_", "."))
|
return type(self).from_string(self.string.replace("-", ".").replace("_", "."))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def underscored(self):
|
def underscored(self) -> "StandardVersion":
|
||||||
"""The underscored representation of the version.
|
"""The underscored representation of the version.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@ -333,7 +453,7 @@ def underscored(self):
|
|||||||
return type(self).from_string(self.string.replace(".", "_").replace("-", "_"))
|
return type(self).from_string(self.string.replace(".", "_").replace("-", "_"))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dashed(self):
|
def dashed(self) -> "StandardVersion":
|
||||||
"""The dashed representation of the version.
|
"""The dashed representation of the version.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@ -347,7 +467,7 @@ def dashed(self):
|
|||||||
return type(self).from_string(self.string.replace(".", "-").replace("_", "-"))
|
return type(self).from_string(self.string.replace(".", "-").replace("_", "-"))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def joined(self):
|
def joined(self) -> "StandardVersion":
|
||||||
"""The joined representation of the version.
|
"""The joined representation of the version.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@ -362,7 +482,7 @@ def joined(self):
|
|||||||
self.string.replace(".", "").replace("-", "").replace("_", "")
|
self.string.replace(".", "").replace("-", "").replace("_", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
def up_to(self, index):
|
def up_to(self, index: int) -> "StandardVersion":
|
||||||
"""The version up to the specified component.
|
"""The version up to the specified component.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@ -482,7 +602,7 @@ def ref_version(self) -> StandardVersion:
|
|||||||
)
|
)
|
||||||
return self._ref_version
|
return self._ref_version
|
||||||
|
|
||||||
def intersects(self, other):
|
def intersects(self, other: VersionType) -> bool:
|
||||||
# For concrete things intersects = satisfies = equality
|
# For concrete things intersects = satisfies = equality
|
||||||
if isinstance(other, GitVersion):
|
if isinstance(other, GitVersion):
|
||||||
return self == other
|
return self == other
|
||||||
@ -492,19 +612,14 @@ def intersects(self, other):
|
|||||||
return self.ref_version.intersects(other)
|
return self.ref_version.intersects(other)
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return any(self.intersects(rhs) for rhs in other)
|
return any(self.intersects(rhs) for rhs in other)
|
||||||
raise ValueError(f"Unexpected type {type(other)}")
|
raise TypeError(f"'intersects()' not supported for instances of {type(other)}")
|
||||||
|
|
||||||
def intersection(self, other):
|
def intersection(self, other: VersionType) -> VersionType:
|
||||||
if isinstance(other, ConcreteVersion):
|
if isinstance(other, ConcreteVersion):
|
||||||
return self if self == other else VersionList()
|
return self if self == other else VersionList()
|
||||||
return other.intersection(self)
|
return other.intersection(self)
|
||||||
|
|
||||||
def overlaps(self, other) -> bool:
|
def satisfies(self, other: VersionType) -> bool:
|
||||||
return self.intersects(other)
|
|
||||||
|
|
||||||
def satisfies(
|
|
||||||
self, other: Union["GitVersion", StandardVersion, "ClosedOpenRange", "VersionList"]
|
|
||||||
):
|
|
||||||
# Concrete versions mean we have to do an equality check
|
# Concrete versions mean we have to do an equality check
|
||||||
if isinstance(other, GitVersion):
|
if isinstance(other, GitVersion):
|
||||||
return self == other
|
return self == other
|
||||||
@ -514,9 +629,9 @@ def satisfies(
|
|||||||
return self.ref_version.satisfies(other)
|
return self.ref_version.satisfies(other)
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return any(self.satisfies(rhs) for rhs in other)
|
return any(self.satisfies(rhs) for rhs in other)
|
||||||
raise ValueError(f"Unexpected type {type(other)}")
|
raise TypeError(f"'satisfies()' not supported for instances of {type(other)}")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
s = f"git.{self.ref}" if self.has_git_prefix else self.ref
|
s = f"git.{self.ref}" if self.has_git_prefix else self.ref
|
||||||
# Note: the solver actually depends on str(...) to produce the effective version.
|
# Note: the solver actually depends on str(...) to produce the effective version.
|
||||||
# So when a lookup is attached, we require the resolved version to be printed.
|
# So when a lookup is attached, we require the resolved version to be printed.
|
||||||
@ -534,7 +649,7 @@ def __repr__(self):
|
|||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: object) -> bool:
|
||||||
# GitVersion cannot be equal to StandardVersion, otherwise == is not transitive
|
# GitVersion cannot be equal to StandardVersion, otherwise == is not transitive
|
||||||
return (
|
return (
|
||||||
isinstance(other, GitVersion)
|
isinstance(other, GitVersion)
|
||||||
@ -542,10 +657,10 @@ def __eq__(self, other):
|
|||||||
and self.ref_version == other.ref_version
|
and self.ref_version == other.ref_version
|
||||||
)
|
)
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other: object) -> bool:
|
||||||
return not self == other
|
return not self == other
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other: object) -> bool:
|
||||||
if isinstance(other, GitVersion):
|
if isinstance(other, GitVersion):
|
||||||
return (self.ref_version, self.ref) < (other.ref_version, other.ref)
|
return (self.ref_version, self.ref) < (other.ref_version, other.ref)
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
@ -553,9 +668,9 @@ def __lt__(self, other):
|
|||||||
return self.ref_version < other
|
return self.ref_version < other
|
||||||
if isinstance(other, ClosedOpenRange):
|
if isinstance(other, ClosedOpenRange):
|
||||||
return self.ref_version < other
|
return self.ref_version < other
|
||||||
raise ValueError(f"Unexpected type {type(other)}")
|
raise TypeError(f"'<' not supported between instances of {type(self)} and {type(other)}")
|
||||||
|
|
||||||
def __le__(self, other):
|
def __le__(self, other: object) -> bool:
|
||||||
if isinstance(other, GitVersion):
|
if isinstance(other, GitVersion):
|
||||||
return (self.ref_version, self.ref) <= (other.ref_version, other.ref)
|
return (self.ref_version, self.ref) <= (other.ref_version, other.ref)
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
@ -564,9 +679,9 @@ def __le__(self, other):
|
|||||||
if isinstance(other, ClosedOpenRange):
|
if isinstance(other, ClosedOpenRange):
|
||||||
# Equality is not a thing
|
# Equality is not a thing
|
||||||
return self.ref_version < other
|
return self.ref_version < other
|
||||||
raise ValueError(f"Unexpected type {type(other)}")
|
raise TypeError(f"'<=' not supported between instances of {type(self)} and {type(other)}")
|
||||||
|
|
||||||
def __ge__(self, other):
|
def __ge__(self, other: object) -> bool:
|
||||||
if isinstance(other, GitVersion):
|
if isinstance(other, GitVersion):
|
||||||
return (self.ref_version, self.ref) >= (other.ref_version, other.ref)
|
return (self.ref_version, self.ref) >= (other.ref_version, other.ref)
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
@ -574,9 +689,9 @@ def __ge__(self, other):
|
|||||||
return self.ref_version >= other
|
return self.ref_version >= other
|
||||||
if isinstance(other, ClosedOpenRange):
|
if isinstance(other, ClosedOpenRange):
|
||||||
return self.ref_version > other
|
return self.ref_version > other
|
||||||
raise ValueError(f"Unexpected type {type(other)}")
|
raise TypeError(f"'>=' not supported between instances of {type(self)} and {type(other)}")
|
||||||
|
|
||||||
def __gt__(self, other):
|
def __gt__(self, other: object) -> bool:
|
||||||
if isinstance(other, GitVersion):
|
if isinstance(other, GitVersion):
|
||||||
return (self.ref_version, self.ref) > (other.ref_version, other.ref)
|
return (self.ref_version, self.ref) > (other.ref_version, other.ref)
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
@ -584,14 +699,14 @@ def __gt__(self, other):
|
|||||||
return self.ref_version >= other
|
return self.ref_version >= other
|
||||||
if isinstance(other, ClosedOpenRange):
|
if isinstance(other, ClosedOpenRange):
|
||||||
return self.ref_version > other
|
return self.ref_version > other
|
||||||
raise ValueError(f"Unexpected type {type(other)}")
|
raise TypeError(f"'>' not supported between instances of {type(self)} and {type(other)}")
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
# hashing should not cause version lookup
|
# hashing should not cause version lookup
|
||||||
return hash(self.ref)
|
return hash(self.ref)
|
||||||
|
|
||||||
def __contains__(self, other):
|
def __contains__(self, other: object) -> bool:
|
||||||
raise Exception("Not implemented yet")
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ref_lookup(self):
|
def ref_lookup(self):
|
||||||
@ -649,7 +764,7 @@ def up_to(self, index) -> StandardVersion:
|
|||||||
return self.ref_version.up_to(index)
|
return self.ref_version.up_to(index)
|
||||||
|
|
||||||
|
|
||||||
class ClosedOpenRange:
|
class ClosedOpenRange(VersionType):
|
||||||
def __init__(self, lo: StandardVersion, hi: StandardVersion):
|
def __init__(self, lo: StandardVersion, hi: StandardVersion):
|
||||||
if hi < lo:
|
if hi < lo:
|
||||||
raise EmptyRangeError(f"{lo}..{hi} is an empty range")
|
raise EmptyRangeError(f"{lo}..{hi} is an empty range")
|
||||||
@ -657,14 +772,14 @@ def __init__(self, lo: StandardVersion, hi: StandardVersion):
|
|||||||
self.hi: StandardVersion = hi
|
self.hi: StandardVersion = hi
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_version_range(cls, lo: StandardVersion, hi: StandardVersion):
|
def from_version_range(cls, lo: StandardVersion, hi: StandardVersion) -> "ClosedOpenRange":
|
||||||
"""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) -> str:
|
||||||
# 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)
|
||||||
@ -726,9 +841,9 @@ def __gt__(self, other):
|
|||||||
def __contains__(rhs, lhs):
|
def __contains__(rhs, lhs):
|
||||||
if isinstance(lhs, (ConcreteVersion, ClosedOpenRange, VersionList)):
|
if isinstance(lhs, (ConcreteVersion, ClosedOpenRange, VersionList)):
|
||||||
return lhs.satisfies(rhs)
|
return lhs.satisfies(rhs)
|
||||||
raise ValueError(f"Unexpected type {type(lhs)}")
|
raise TypeError(f"'in' not supported between instances of {type(rhs)} and {type(lhs)}")
|
||||||
|
|
||||||
def intersects(self, other: Union[ConcreteVersion, "ClosedOpenRange", "VersionList"]):
|
def intersects(self, other: VersionType) -> bool:
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
return self.lo <= other < self.hi
|
return self.lo <= other < self.hi
|
||||||
if isinstance(other, GitVersion):
|
if isinstance(other, GitVersion):
|
||||||
@ -737,23 +852,18 @@ def intersects(self, other: Union[ConcreteVersion, "ClosedOpenRange", "VersionLi
|
|||||||
return (self.lo < other.hi) and (other.lo < self.hi)
|
return (self.lo < other.hi) and (other.lo < self.hi)
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return any(self.intersects(rhs) for rhs in other)
|
return any(self.intersects(rhs) for rhs in other)
|
||||||
raise ValueError(f"Unexpected type {type(other)}")
|
raise TypeError(f"'intersects' not supported for instances of {type(other)}")
|
||||||
|
|
||||||
def satisfies(self, other: Union["ClosedOpenRange", ConcreteVersion, "VersionList"]):
|
def satisfies(self, other: VersionType) -> bool:
|
||||||
if isinstance(other, ConcreteVersion):
|
if isinstance(other, ConcreteVersion):
|
||||||
return False
|
return False
|
||||||
if isinstance(other, ClosedOpenRange):
|
if isinstance(other, ClosedOpenRange):
|
||||||
return not (self.lo < other.lo or other.hi < self.hi)
|
return not (self.lo < other.lo or other.hi < self.hi)
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return any(self.satisfies(rhs) for rhs in other)
|
return any(self.satisfies(rhs) for rhs in other)
|
||||||
raise ValueError(other)
|
raise TypeError(f"'satisfies()' not supported for instances of {type(other)}")
|
||||||
|
|
||||||
def overlaps(self, other: Union["ClosedOpenRange", ConcreteVersion, "VersionList"]) -> bool:
|
def _union_if_not_disjoint(self, other: VersionType) -> Optional["ClosedOpenRange"]:
|
||||||
return self.intersects(other)
|
|
||||||
|
|
||||||
def _union_if_not_disjoint(
|
|
||||||
self, other: Union["ClosedOpenRange", ConcreteVersion]
|
|
||||||
) -> Optional["ClosedOpenRange"]:
|
|
||||||
"""Same as union, but returns None when the union is not connected. This function is not
|
"""Same as union, but returns None when the union is not connected. This function is not
|
||||||
implemented for version lists as right-hand side, as that makes little sense."""
|
implemented for version lists as right-hand side, as that makes little sense."""
|
||||||
if isinstance(other, StandardVersion):
|
if isinstance(other, StandardVersion):
|
||||||
@ -770,9 +880,9 @@ def _union_if_not_disjoint(
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
raise TypeError(f"Unexpected type {type(other)}")
|
raise TypeError(f"'union()' not supported for instances of {type(other)}")
|
||||||
|
|
||||||
def union(self, other: Union["ClosedOpenRange", ConcreteVersion, "VersionList"]):
|
def union(self, other: VersionType) -> VersionType:
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
v = other.copy()
|
v = other.copy()
|
||||||
v.add(self)
|
v.add(self)
|
||||||
@ -781,35 +891,51 @@ def union(self, other: Union["ClosedOpenRange", ConcreteVersion, "VersionList"])
|
|||||||
result = self._union_if_not_disjoint(other)
|
result = self._union_if_not_disjoint(other)
|
||||||
return result if result is not None else VersionList([self, other])
|
return result if result is not None else VersionList([self, other])
|
||||||
|
|
||||||
def intersection(self, other: Union["ClosedOpenRange", ConcreteVersion]):
|
def intersection(self, other: VersionType) -> VersionType:
|
||||||
# range - version -> singleton or nothing.
|
# range - version -> singleton or nothing.
|
||||||
|
if isinstance(other, ClosedOpenRange):
|
||||||
|
# range - range -> range or nothing.
|
||||||
|
max_lo = max(self.lo, other.lo)
|
||||||
|
min_hi = min(self.hi, other.hi)
|
||||||
|
return ClosedOpenRange(max_lo, min_hi) if max_lo < min_hi else VersionList()
|
||||||
|
|
||||||
if isinstance(other, ConcreteVersion):
|
if isinstance(other, ConcreteVersion):
|
||||||
return other if self.intersects(other) else VersionList()
|
return other if self.intersects(other) else VersionList()
|
||||||
|
|
||||||
# range - range -> range or nothing.
|
raise TypeError(f"'intersection()' not supported for instances of {type(other)}")
|
||||||
max_lo = max(self.lo, other.lo)
|
|
||||||
min_hi = min(self.hi, other.hi)
|
|
||||||
return ClosedOpenRange(max_lo, min_hi) if max_lo < min_hi else VersionList()
|
|
||||||
|
|
||||||
|
|
||||||
class VersionList:
|
class VersionList(VersionType):
|
||||||
"""Sorted, non-redundant list of Version and ClosedOpenRange elements."""
|
"""Sorted, non-redundant list of Version and ClosedOpenRange elements."""
|
||||||
|
|
||||||
def __init__(self, vlist=None):
|
versions: List[VersionType]
|
||||||
self.versions: List[Union[StandardVersion, GitVersion, ClosedOpenRange]] = []
|
|
||||||
|
def __init__(self, vlist: Optional[Union[str, VersionType, Iterable]] = None):
|
||||||
if vlist is None:
|
if vlist is None:
|
||||||
pass
|
self.versions = []
|
||||||
|
|
||||||
elif isinstance(vlist, str):
|
elif isinstance(vlist, str):
|
||||||
vlist = from_string(vlist)
|
vlist = from_string(vlist)
|
||||||
if isinstance(vlist, VersionList):
|
if isinstance(vlist, VersionList):
|
||||||
self.versions = vlist.versions
|
self.versions = vlist.versions
|
||||||
else:
|
else:
|
||||||
self.versions = [vlist]
|
self.versions = [vlist]
|
||||||
else:
|
|
||||||
|
elif isinstance(vlist, (ConcreteVersion, ClosedOpenRange)):
|
||||||
|
self.versions = [vlist]
|
||||||
|
|
||||||
|
elif isinstance(vlist, VersionList):
|
||||||
|
self.versions = vlist[:]
|
||||||
|
|
||||||
|
elif isinstance(vlist, Iterable):
|
||||||
|
self.versions = []
|
||||||
for v in vlist:
|
for v in vlist:
|
||||||
self.add(ver(v))
|
self.add(ver(v))
|
||||||
|
|
||||||
def add(self, item: Union[StandardVersion, GitVersion, ClosedOpenRange, "VersionList"]):
|
else:
|
||||||
|
raise TypeError(f"Cannot construct VersionList from {type(vlist)}")
|
||||||
|
|
||||||
|
def add(self, item: VersionType) -> None:
|
||||||
if isinstance(item, (StandardVersion, GitVersion)):
|
if isinstance(item, (StandardVersion, GitVersion)):
|
||||||
i = bisect_left(self, item)
|
i = bisect_left(self, item)
|
||||||
# Only insert when prev and next are not intersected.
|
# Only insert when prev and next are not intersected.
|
||||||
@ -865,7 +991,7 @@ def concrete_range_as_version(self) -> Optional[ConcreteVersion]:
|
|||||||
return v.lo
|
return v.lo
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def copy(self):
|
def copy(self) -> "VersionList":
|
||||||
return VersionList(self)
|
return VersionList(self)
|
||||||
|
|
||||||
def lowest(self) -> Optional[StandardVersion]:
|
def lowest(self) -> Optional[StandardVersion]:
|
||||||
@ -889,7 +1015,7 @@ def preferred(self) -> Optional[StandardVersion]:
|
|||||||
"""Get the preferred (latest) version in the list."""
|
"""Get the preferred (latest) version in the list."""
|
||||||
return self.highest_numeric() or self.highest()
|
return self.highest_numeric() or self.highest()
|
||||||
|
|
||||||
def satisfies(self, other) -> bool:
|
def satisfies(self, other: VersionType) -> bool:
|
||||||
# This exploits the fact that version lists are "reduced" and normalized, so we can
|
# This exploits the fact that version lists are "reduced" and normalized, so we can
|
||||||
# never have a list like [1:3, 2:4] since that would be normalized to [1:4]
|
# never have a list like [1:3, 2:4] since that would be normalized to [1:4]
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
@ -898,9 +1024,9 @@ def satisfies(self, other) -> bool:
|
|||||||
if isinstance(other, (ConcreteVersion, ClosedOpenRange)):
|
if isinstance(other, (ConcreteVersion, ClosedOpenRange)):
|
||||||
return all(lhs.satisfies(other) for lhs in self)
|
return all(lhs.satisfies(other) for lhs in self)
|
||||||
|
|
||||||
raise ValueError(f"Unsupported type {type(other)}")
|
raise TypeError(f"'satisfies()' not supported for instances of {type(other)}")
|
||||||
|
|
||||||
def intersects(self, other):
|
def intersects(self, other: VersionType) -> bool:
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
s = o = 0
|
s = o = 0
|
||||||
while s < len(self) and o < len(other):
|
while s < len(self) and o < len(other):
|
||||||
@ -915,19 +1041,16 @@ def intersects(self, other):
|
|||||||
if isinstance(other, (ClosedOpenRange, StandardVersion)):
|
if isinstance(other, (ClosedOpenRange, StandardVersion)):
|
||||||
return any(v.intersects(other) for v in self)
|
return any(v.intersects(other) for v in self)
|
||||||
|
|
||||||
raise ValueError(f"Unsupported type {type(other)}")
|
raise TypeError(f"'intersects()' not supported for instances of {type(other)}")
|
||||||
|
|
||||||
def overlaps(self, other) -> bool:
|
def to_dict(self) -> Dict:
|
||||||
return self.intersects(other)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
"""Generate human-readable dict for YAML."""
|
"""Generate human-readable dict for YAML."""
|
||||||
if self.concrete:
|
if self.concrete:
|
||||||
return syaml_dict([("version", str(self[0]))])
|
return syaml_dict([("version", str(self[0]))])
|
||||||
return syaml_dict([("versions", [str(v) for v in self])])
|
return syaml_dict([("versions", [str(v) for v in self])])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(dictionary):
|
def from_dict(dictionary) -> "VersionList":
|
||||||
"""Parse dict from to_dict."""
|
"""Parse dict from to_dict."""
|
||||||
if "versions" in dictionary:
|
if "versions" in dictionary:
|
||||||
return VersionList(dictionary["versions"])
|
return VersionList(dictionary["versions"])
|
||||||
@ -935,27 +1058,29 @@ def from_dict(dictionary):
|
|||||||
return VersionList([Version(dictionary["version"])])
|
return VersionList([Version(dictionary["version"])])
|
||||||
raise ValueError("Dict must have 'version' or 'versions' in it.")
|
raise ValueError("Dict must have 'version' or 'versions' in it.")
|
||||||
|
|
||||||
def update(self, other: "VersionList"):
|
def update(self, other: "VersionList") -> None:
|
||||||
for v in other.versions:
|
self.add(other)
|
||||||
self.add(v)
|
|
||||||
|
|
||||||
def union(self, other: "VersionList"):
|
def union(self, other: VersionType) -> VersionType:
|
||||||
result = self.copy()
|
result = self.copy()
|
||||||
result.update(other)
|
result.add(other)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def intersection(self, other: "VersionList") -> "VersionList":
|
def intersection(self, other: VersionType) -> "VersionList":
|
||||||
result = VersionList()
|
result = VersionList()
|
||||||
for lhs, rhs in ((self, other), (other, self)):
|
if isinstance(other, VersionList):
|
||||||
for x in lhs:
|
for lhs, rhs in ((self, other), (other, self)):
|
||||||
i = bisect_left(rhs.versions, x)
|
for x in lhs:
|
||||||
if i > 0:
|
i = bisect_left(rhs.versions, x)
|
||||||
result.add(rhs[i - 1].intersection(x))
|
if i > 0:
|
||||||
if i < len(rhs):
|
result.add(rhs[i - 1].intersection(x))
|
||||||
result.add(rhs[i].intersection(x))
|
if i < len(rhs):
|
||||||
return result
|
result.add(rhs[i].intersection(x))
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return self.intersection(VersionList(other))
|
||||||
|
|
||||||
def intersect(self, other) -> bool:
|
def intersect(self, other: VersionType) -> bool:
|
||||||
"""Intersect this spec's list with other.
|
"""Intersect this spec's list with other.
|
||||||
|
|
||||||
Return True if the spec changed as a result; False otherwise
|
Return True if the spec changed as a result; False otherwise
|
||||||
@ -965,6 +1090,7 @@ def intersect(self, other) -> bool:
|
|||||||
self.versions = isection.versions
|
self.versions = isection.versions
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
# typing this and getitem are a pain in Python 3.6
|
||||||
def __contains__(self, other):
|
def __contains__(self, other):
|
||||||
if isinstance(other, (ClosedOpenRange, StandardVersion)):
|
if isinstance(other, (ClosedOpenRange, StandardVersion)):
|
||||||
i = bisect_left(self, other)
|
i = bisect_left(self, other)
|
||||||
@ -978,52 +1104,52 @@ def __contains__(self, other):
|
|||||||
def __getitem__(self, index):
|
def __getitem__(self, index):
|
||||||
return self.versions[index]
|
return self.versions[index]
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self) -> Iterator:
|
||||||
return iter(self.versions)
|
return iter(self.versions)
|
||||||
|
|
||||||
def __reversed__(self):
|
def __reversed__(self) -> Iterator:
|
||||||
return reversed(self.versions)
|
return reversed(self.versions)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
return len(self.versions)
|
return len(self.versions)
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self) -> bool:
|
||||||
return bool(self.versions)
|
return bool(self.versions)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other) -> bool:
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return self.versions == other.versions
|
return self.versions == other.versions
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other) -> bool:
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return self.versions != other.versions
|
return self.versions != other.versions
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other) -> bool:
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return self.versions < other.versions
|
return self.versions < other.versions
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __le__(self, other):
|
def __le__(self, other) -> bool:
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return self.versions <= other.versions
|
return self.versions <= other.versions
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __ge__(self, other):
|
def __ge__(self, other) -> bool:
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return self.versions >= other.versions
|
return self.versions >= other.versions
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __gt__(self, other):
|
def __gt__(self, other) -> bool:
|
||||||
if isinstance(other, VersionList):
|
if isinstance(other, VersionList):
|
||||||
return self.versions > other.versions
|
return self.versions > other.versions
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
return hash(tuple(self.versions))
|
return hash(tuple(self.versions))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
if not self.versions:
|
if not self.versions:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@ -1031,7 +1157,7 @@ def __str__(self):
|
|||||||
f"={v}" if isinstance(v, StandardVersion) else str(v) for v in self.versions
|
f"={v}" if isinstance(v, StandardVersion) else str(v) for v in self.versions
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return str(self.versions)
|
return str(self.versions)
|
||||||
|
|
||||||
|
|
||||||
@ -1106,12 +1232,10 @@ def _next_version(v: StandardVersion) -> StandardVersion:
|
|||||||
release = release[:-1] + (_next_version_str_component(release[-1]),)
|
release = release[:-1] + (_next_version_str_component(release[-1]),)
|
||||||
else:
|
else:
|
||||||
release = release[:-1] + (release[-1] + 1,)
|
release = release[:-1] + (release[-1] + 1,)
|
||||||
components = [""] * (2 * len(release))
|
|
||||||
components[::2] = release
|
# Avoid constructing a string here for performance. Instead, pass "" to
|
||||||
components[1::2] = separators[: len(release)]
|
# StandardVersion to lazily stringify.
|
||||||
if prerelease_type != FINAL:
|
return StandardVersion("", (release, prerelease), separators)
|
||||||
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:
|
def _prev_version(v: StandardVersion) -> StandardVersion:
|
||||||
@ -1130,19 +1254,15 @@ def _prev_version(v: StandardVersion) -> StandardVersion:
|
|||||||
release = release[:-1] + (_prev_version_str_component(release[-1]),)
|
release = release[:-1] + (_prev_version_str_component(release[-1]),)
|
||||||
else:
|
else:
|
||||||
release = release[:-1] + (release[-1] - 1,)
|
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:]))
|
|
||||||
|
|
||||||
# this is only used for comparison functions, so don't bother making a string
|
# Avoid constructing a string here for performance. Instead, pass "" to
|
||||||
return StandardVersion(None, (release, prerelease), separators)
|
# StandardVersion to lazily stringify.
|
||||||
|
return StandardVersion("", (release, prerelease), separators)
|
||||||
|
|
||||||
|
|
||||||
def Version(string: Union[str, int]) -> Union[GitVersion, StandardVersion]:
|
def Version(string: Union[str, int]) -> ConcreteVersion:
|
||||||
if not isinstance(string, (str, int)):
|
if not isinstance(string, (str, int)):
|
||||||
raise ValueError(f"Cannot construct a version from {type(string)}")
|
raise TypeError(f"Cannot construct a version from {type(string)}")
|
||||||
string = str(string)
|
string = str(string)
|
||||||
if is_git_version(string):
|
if is_git_version(string):
|
||||||
return GitVersion(string)
|
return GitVersion(string)
|
||||||
@ -1155,7 +1275,7 @@ def VersionRange(lo: Union[str, StandardVersion], hi: Union[str, StandardVersion
|
|||||||
return ClosedOpenRange.from_version_range(lo, hi)
|
return ClosedOpenRange.from_version_range(lo, hi)
|
||||||
|
|
||||||
|
|
||||||
def from_string(string) -> Union[VersionList, ClosedOpenRange, StandardVersion, GitVersion]:
|
def from_string(string: str) -> VersionType:
|
||||||
"""Converts a string to a version object. This is private. Client code should use ver()."""
|
"""Converts a string to a version object. This is private. Client code should use ver()."""
|
||||||
string = string.replace(" ", "")
|
string = string.replace(" ", "")
|
||||||
|
|
||||||
@ -1184,17 +1304,17 @@ def from_string(string) -> Union[VersionList, ClosedOpenRange, StandardVersion,
|
|||||||
return VersionRange(v, v)
|
return VersionRange(v, v)
|
||||||
|
|
||||||
|
|
||||||
def ver(obj) -> Union[VersionList, ClosedOpenRange, StandardVersion, GitVersion]:
|
def ver(obj: Union[VersionType, str, list, tuple, int, float]) -> VersionType:
|
||||||
"""Parses a Version, VersionRange, or VersionList from a string
|
"""Parses a Version, VersionRange, or VersionList from a string
|
||||||
or list of strings.
|
or list of strings.
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, (list, tuple)):
|
if isinstance(obj, VersionType):
|
||||||
return VersionList(obj)
|
return obj
|
||||||
elif isinstance(obj, str):
|
elif isinstance(obj, str):
|
||||||
return from_string(obj)
|
return from_string(obj)
|
||||||
|
elif isinstance(obj, (list, tuple)):
|
||||||
|
return VersionList(obj)
|
||||||
elif isinstance(obj, (int, float)):
|
elif isinstance(obj, (int, float)):
|
||||||
return from_string(str(obj))
|
return from_string(str(obj))
|
||||||
elif isinstance(obj, (StandardVersion, GitVersion, ClosedOpenRange, VersionList)):
|
|
||||||
return obj
|
|
||||||
else:
|
else:
|
||||||
raise TypeError("ver() can't convert %s to version!" % type(obj))
|
raise TypeError("ver() can't convert %s to version!" % type(obj))
|
||||||
|
Loading…
Reference in New Issue
Block a user