diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b495b28b..20ddc8c9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +Version v31.0.0 +---------------- + +- Raise instead of returning ``NotImplementedError`` in ``version_range.VersionRange`` methods. https://github.com/aboutcode-org/univers/pull/158 + Version v30.12.1 ---------------- diff --git a/requirements-dev.txt b/requirements-dev.txt index 82a59db3..c16d9a34 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,8 +7,7 @@ pytest==7.2.0 pytest-forked==1.4.0 pytest-xdist==3.2.0 toml==0.10.2 -typing-extensions==3.10.0.2 +typing-extensions==4.1.0 zipp==3.6.0 -black==22.3.0 typed-ast==1.4.3 pathspec==0.9.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f253417d..04cd23d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ attrs==21.2.0 -packaging==21.0 +packaging==25.0 pyparsing==2.4.7 semantic-version==2.8.5 semver==2.13.0 diff --git a/src/univers/arch.py b/src/univers/arch.py index 3eae6c9f..32789ade 100644 --- a/src/univers/arch.py +++ b/src/univers/arch.py @@ -6,6 +6,8 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + import re from itertools import zip_longest from typing import Dict diff --git a/src/univers/conan/errors.py b/src/univers/conan/errors.py index 40d1e886..fd3ee552 100644 --- a/src/univers/conan/errors.py +++ b/src/univers/conan/errors.py @@ -5,26 +5,42 @@ # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. """ - Exceptions raised and handled in Conan - These exceptions are mapped between server (as an HTTP response) and client - through the REST API. When an error happens in server its translated to an HTTP - error code that its sent to client. Client reads the server code and raise the - matching exception. +Exceptions raised and handled in Conan +These exceptions are mapped between server (as an HTTP response) and client +through the REST API. When an error happens in server its translated to an HTTP +error code that its sent to client. Client reads the server code and raise the +matching exception. - see return_plugin.py +see return_plugin.py """ +from __future__ import annotations + from contextlib import contextmanager +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterable + from collections.abc import Iterator + from typing import Any + from typing import Callable + + try: + from typing import Never + except ImportError: + from typing_extensions import Never @contextmanager -def conanfile_remove_attr(conanfile, names, method): +def conanfile_remove_attr( + conanfile: Any, names: Iterable[str], method: Callable[..., Any] +) -> Iterator[Never]: """remove some self.xxxx attribute from the class, so it raises an exception if used within a given conanfile method """ original_class = type(conanfile) - def _prop(attr_name): + def _prop(attr_name: str) -> property: def _m(_): raise ConanException(f"'self.{attr_name}' access in '{method}()' method is forbidden") @@ -41,7 +57,7 @@ def _m(_): @contextmanager -def conanfile_exception_formatter(conanfile_name, func_name): +def conanfile_exception_formatter(conanfile_name: str, func_name: str) -> Iterator[Never]: """ Decorator to throw an exception formatted with the line of the conanfile where the error ocurrs. """ @@ -74,7 +90,9 @@ def _raise_conanfile_exc(e): _raise_conanfile_exc(exc) -def _format_conanfile_exception(scope, method, exception): +def _format_conanfile_exception( + scope: str, method: Callable[..., Any], exception: BaseException +) -> str: """ It will iterate the traceback lines, when it finds that the source code is inside the users conanfile it "start recording" the messages, when the trace exits the conanfile we return @@ -125,12 +143,12 @@ def __init__(self, *args, **kwargs): self.remote = kwargs.pop("remote", None) super(ConanException, self).__init__(*args, **kwargs) - def remote_message(self): + def remote_message(self) -> str: if self.remote: return " [Remote: {}]".format(self.remote.name) return "" - def __str__(self): + def __str__(self) -> str: msg = super(ConanException, self).__str__() @@ -242,7 +260,7 @@ class UserInterfaceErrorException(RequestErrorException): pass -EXCEPTION_CODE_MAPPING = { +EXCEPTION_CODE_MAPPING: dict[ConanException, int] = { InternalErrorException: 500, RequestErrorException: 400, AuthenticationException: 401, diff --git a/src/univers/conan/version.py b/src/univers/conan/version.py index 7cac8635..3dac0d43 100644 --- a/src/univers/conan/version.py +++ b/src/univers/conan/version.py @@ -4,10 +4,19 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + from functools import total_ordering +from typing import TYPE_CHECKING from univers.conan.errors import ConanException +if TYPE_CHECKING: + try: + from typing import Self + except ImportError: + from typing_extensions import Self + @total_ordering class _VersionItem: @@ -15,32 +24,32 @@ class _VersionItem: They can be int or strings """ - def __init__(self, item): + def __init__(self, item: str | int | Self): try: self._v = int(item) except ValueError: self._v = item @property - def value(self): + def value(self) -> int: return self._v - def __str__(self): + def __str__(self) -> str: return str(self._v) - def __add__(self, other): + def __add__(self, other: Self) -> int: # necessary for the "bump()" functionality. Other aritmetic operations are missing return self._v + other - def __eq__(self, other): + def __eq__(self, other: str | int | Self) -> bool: if not isinstance(other, _VersionItem): other = _VersionItem(other) return self._v == other._v - def __hash__(self): + def __hash__(self) -> int: return hash(self._v) - def __lt__(self, other): + def __lt__(self, other: str | int | Self) -> bool: """ @type other: _VersionItem """ @@ -59,7 +68,7 @@ class Version: It is just a helper to parse "." or "-" and compare taking into account integers when possible """ - def __init__(self, value): + def __init__(self, value: int | str | Self): value = str(value) self._value = value @@ -85,7 +94,7 @@ def __init__(self, value): del items[-1] self._nonzero_items = tuple(items) - def bump(self, index): + def bump(self, index: int) -> Self: """ :meta private: Bump the version @@ -110,7 +119,7 @@ def bump(self, index): result = Version(v) return result - def upper_bound(self, index): + def upper_bound(self, index: int) -> Self: items = list(self._items[:index]) try: items.append(self._items[index] + 1) @@ -123,52 +132,52 @@ def upper_bound(self, index): return result @property - def pre(self): + def pre(self) -> Self | None: return self._pre @property - def build(self): + def build(self) -> Self | None: return self._build @property - def main(self): + def main(self) -> tuple[_VersionItem, ...]: return self._items @property - def major(self): + def major(self) -> _VersionItem | None: try: return self.main[0] except IndexError: return None @property - def minor(self): + def minor(self) -> _VersionItem | None: try: return self.main[1] except IndexError: return None @property - def patch(self): + def patch(self) -> _VersionItem | None: try: return self.main[2] except IndexError: return None @property - def micro(self): + def micro(self) -> _VersionItem | None: try: return self.main[3] except IndexError: return None - def __str__(self): + def __str__(self) -> str: return self._value - def __repr__(self): + def __repr__(self) -> str: return self._value - def __eq__(self, other): + def __eq__(self, other: int | str | Self | None) -> bool: if other is None: return False if not isinstance(other, Version): @@ -180,10 +189,10 @@ def __eq__(self, other): other._build, ) - def __hash__(self): + def __hash__(self) -> int: return hash((self._nonzero_items, self._pre, self._build)) - def __lt__(self, other): + def __lt__(self, other: int | str | Self | None) -> bool: if other is None: return False if not isinstance(other, Version): diff --git a/src/univers/conan/version_range.py b/src/univers/conan/version_range.py index f64980fa..23890f35 100644 --- a/src/univers/conan/version_range.py +++ b/src/univers/conan/version_range.py @@ -4,6 +4,8 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + from collections import namedtuple from univers.conan.errors import ConanException @@ -13,7 +15,7 @@ class _ConditionSet: - def __init__(self, expression, prerelease): + def __init__(self, expression: str, prerelease: bool): expressions = expression.split() self.prerelease = prerelease self.conditions = [] @@ -25,7 +27,7 @@ def __init__(self, expression, prerelease): self.conditions.extend(self._parse_expression(e)) @staticmethod - def _parse_expression(expression): + def _parse_expression(expression: str) -> list[_Condition | ConanVersion]: if expression == "" or expression == "*": return [_Condition(">=", ConanVersion("0.0.0"))] @@ -60,7 +62,7 @@ def first_non_zero(main): else: return [_Condition(operator, ConanVersion(version))] - def valid(self, version): + def valid(self, version: ConanVersion) -> bool: if version.pre: if not self.prerelease: return False @@ -84,7 +86,7 @@ def valid(self, version): class VersionRange: - def __init__(self, expression): + def __init__(self, expression: str): self._expression = expression tokens = expression.split(",") prereleases = None @@ -97,10 +99,10 @@ def __init__(self, expression): for alternative in version_expr.split("||"): self.condition_sets.append(_ConditionSet(alternative, prereleases)) - def __str__(self): + def __str__(self) -> str: return self._expression - def __contains__(self, version): + def __contains__(self, version: ConanVersion) -> bool: assert isinstance(version, ConanVersion), type(version) for condition_set in self.condition_sets: if condition_set.valid(version): diff --git a/src/univers/debian.py b/src/univers/debian.py index 463c2e72..aeb8ce69 100644 --- a/src/univers/debian.py +++ b/src/univers/debian.py @@ -8,16 +8,28 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + import logging import operator as operator_module import re from functools import cmp_to_key from itertools import zip_longest +from typing import TYPE_CHECKING from attr import asdict from attr import attrib from attr import attrs +if TYPE_CHECKING: + from re import Match + + try: + from typing import Self + except ImportError: + from typing_extensions import Self + + logger = logging.getLogger(__name__) """ @@ -85,7 +97,7 @@ class Version(object): upstream = attrib(default=None) revision = attrib(default="0") - def __str__(self, *args, **kwargs): + def __str__(self, *args, **kwargs) -> str: if self.epoch: version = f"{self.epoch}:{self.upstream}" else: @@ -96,42 +108,42 @@ def __str__(self, *args, **kwargs): return version - def __repr__(self, *args, **kwargs): + def __repr__(self, *args, **kwargs) -> str: return str(self) - def __hash__(self): + def __hash__(self) -> int: return hash(self.tuple()) - def __eq__(self, other): + def __eq__(self, other: Self) -> bool: if not isinstance(other, self.__class__): return NotImplemented return self.tuple() == other.tuple() - def __ne__(self, other): + def __ne__(self, other: Self) -> bool: return not self.__eq__(other) - def __lt__(self, other): + def __lt__(self, other: Self) -> bool: if not isinstance(other, self.__class__): return NotImplemented return eval_constraint(self, "<<", other) - def __le__(self, other): + def __le__(self, other: Self) -> bool: if not isinstance(other, self.__class__): return NotImplemented return eval_constraint(self, "<=", other) - def __gt__(self, other): + def __gt__(self, other: Self) -> bool: if not isinstance(other, self.__class__): return NotImplemented return eval_constraint(self, ">>", other) - def __ge__(self, other): + def __ge__(self, other: Self) -> bool: if not isinstance(other, self.__class__): return NotImplemented return eval_constraint(self, ">=", other) @classmethod - def from_string(cls, version): + def from_string(cls, version: str) -> Self: if not version and not isinstance(version, str): raise ValueError('Invalid version string: "{}"'.format(version)) version = version.strip() @@ -154,16 +166,16 @@ def from_string(cls, version): return cls(epoch=epoch, upstream=upstream, revision=revision) @classmethod - def is_valid(cls, version): + def is_valid(cls, version: str) -> Match[str] | None: return is_valid_debian_version(version) - def compare(self, other_version): + def compare(self, other_version: str | Self) -> int: return compare_versions(self, other_version) - def to_dict(self): + def to_dict(self) -> dict[str, str | int | None]: return asdict(self) - def tuple(self): + def tuple(self) -> tuple[int, str | None, str]: return self.epoch, self.upstream, self.revision @@ -185,7 +197,7 @@ def tuple(self): ).match -def eval_constraint(version1, operator, version2): +def eval_constraint(version1: str | Version, operator: str, version2: str | Version) -> bool: """ Evaluate a versions constraint where two Debian package versions are compared with an operator such as < or >. Return True if the constraint is @@ -216,21 +228,21 @@ def eval_constraint(version1, operator, version2): return operator(result, 0) -def compare_versions_key(x): +def compare_versions_key(x): # type: ignore """ Return a key version function suitable for use in sorted(). """ return cmp_to_key(compare_versions)(x) -def compare_strings_key(x): +def compare_strings_key(x): # type: ignore """ Return a key string function suitable for use in sorted(). """ return cmp_to_key(compare_strings)(x) -def compare_strings(version1, version2): +def compare_strings(version1: str, version2: str) -> int: """ Compare two version strings (upstream or revision) using Debian semantics and return one of the following integer numbers: @@ -305,7 +317,7 @@ def compare_strings(version1, version2): return 0 -def compare_versions(version1, version2): +def compare_versions(version1: str | Version, version2: str | Version) -> int: """ Compare two Version objects or strings and return one of the following integer numbers: @@ -319,7 +331,7 @@ def compare_versions(version1, version2): return compare_version_objects(version1, version2) -def coerce_version(value): +def coerce_version(value: str | Version) -> Version: """ Return a Version object from value. @@ -331,7 +343,7 @@ def coerce_version(value): return value -def compare_version_objects(version1, version2): +def compare_version_objects(version1: Version, version2: Version) -> int: """ Compare two Version objects and return one of the following integer numbers: @@ -352,7 +364,7 @@ def compare_version_objects(version1, version2): return 0 -def get_digit_prefix(characters): +def get_digit_prefix(characters: list[str]) -> int: """ Return the digit prefix from a list of characters. """ @@ -362,7 +374,7 @@ def get_digit_prefix(characters): return value -def get_non_digit_prefix(characters): +def get_non_digit_prefix(characters: list[str]) -> list[str]: """ Return the non-digit prefix from a list of characters. """ @@ -373,7 +385,7 @@ def get_non_digit_prefix(characters): # a mapping of characters to integers representing the Debian sort order. -characters_order = { +characters_order: dict[str, int] = { # The tilde sorts before everything. "~": 0, # The empty string sort before everything except a tilde. diff --git a/src/univers/gem.py b/src/univers/gem.py index 2b7259e5..9f31e0a3 100644 --- a/src/univers/gem.py +++ b/src/univers/gem.py @@ -14,10 +14,22 @@ # Originally from https://github.com/rubygems/rubygems and # https://github.com/coi-gov-pl/puppeter +from __future__ import annotations + import operator import re from collections import namedtuple from itertools import dropwhile +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Any + + try: + from typing import Self + except ImportError: + from typing_extensions import Self class InvalidRequirementError(AttributeError): @@ -184,7 +196,7 @@ class GemVersion: VERSION_PATTERN = r"[0-9]+(?:\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?" is_correct = re.compile(rf"^\s*({VERSION_PATTERN})?\s*$").match - def __init__(self, version): + def __init__(self, version: int | str | Self): """ Construct a Version from the ``version`` string. A version string is a series of digits or ASCII letters separated by dots and may contain dash @@ -208,37 +220,37 @@ def __init__(self, version): self.version = version.replace("-", ".pre.") - def __str__(self): + def __str__(self) -> str: return self.original to_string = __str__ - def __repr__(self): + def __repr__(self) -> str: return f"GemVersion({self.original!r})" - def equal_strictly(self, other): + def equal_strictly(self, other: Self) -> bool: return self.version == other.version - def __hash__(self): + def __hash__(self) -> int: return hash(self.canonical_segments) - def __eq__(self, other): + def __eq__(self, other: Self) -> bool: return self.canonical_segments == other.canonical_segments - def __lt__(self, other): + def __lt__(self, other: Self) -> bool: return self.__cmp__(other) < 0 - def __le__(self, other): + def __le__(self, other: Self) -> bool: return self.__cmp__(other) <= 0 - def __gt__(self, other): + def __gt__(self, other: Self) -> bool: return self.__cmp__(other) > 0 - def __ge__(self, other): + def __ge__(self, other: Self) -> bool: return self.__cmp__(other) >= 0 @property - def segments(self): + def segments(self) -> tuple[int | str, ...]: """ Returns segments for this version where segments are ints or strings parsed from the original version string. @@ -251,7 +263,7 @@ def segments(self): return tuple(segments) @property - def canonical_segments(self): + def canonical_segments(self) -> tuple[int | str, ...]: """ Returns "canonical segments" for this version using the Rubygems way for canonicalization. @@ -263,7 +275,7 @@ def canonical_segments(self): canonical_segments.extend(reversed(segs)) return tuple(canonical_segments) - def bump(self): + def bump(self) -> Self: """ Return a new version object where the next to the last revision number is one greater (e.g., 5.3.1 => 5.4) i.e., incrementing this GemVersion @@ -287,7 +299,7 @@ def bump(self): segments = [str(r) for r in segments] return GemVersion(".".join(segments)) - def release(self): + def release(self) -> Self: """ Return a new GemVersion which is the release for this version (e.g., 1.2.0.a -> 1.2.0). Non-prerelease versions return themselves. A release @@ -301,14 +313,14 @@ def release(self): return GemVersion(".".join(segments)) return self - def prerelease(self): + def prerelease(self) -> bool: """ Return True if this is considered as a prerelease version. A version is considered a prerelease if it contains a letter. """ return any(not str(s).isdigit() for s in self.segments) - def split_segments(self): + def split_segments(self) -> tuple[list[str], list[str]]: """ Return a two-tuple of segments: - the first is a list of numeric-only segments starting from the left @@ -328,7 +340,7 @@ def split_segments(self): string_segments.append(seg) return numeric_segments, string_segments - def __cmp__(self, other, trace=False): + def __cmp__(self, other: Any, trace: bool = False) -> int | None: """ Compare this version with ``other`` returning -1, 0, or 1 if the other version is larger, the same, or smaller than this @@ -363,7 +375,7 @@ def __cmp__(self, other, trace=False): if lhsegments == rhsegments: if trace: - print(f" lhsegments == rhsegments: returning 0") + print(" lhsegments == rhsegments: returning 0") return 0 lhsize = len(lhsegments) @@ -410,21 +422,21 @@ def __cmp__(self, other, trace=False): if lhs == rhs: if trace: - print(f" lhs == rhs: continue") + print(" lhs == rhs: continue") continue if isinstance(lhs, str) and isinstance(rhs, int): if trace: print(f" isinstance(lhs, str): {type(lhs)!r}") print(f" isinstance(rhs, int): {type(rhs)!r}") - print(f" return -1") + print(" return -1") return -1 if isinstance(lhs, int) and isinstance(rhs, str): if trace: print(f" isinstance(lhs, int): {type(lhs)!r}") print(f" isinstance(rhs, str): {type(rhs)!r}") - print(f" return 1") + print(" return 1") return 1 result = (lhs > rhs) - (lhs < rhs) @@ -435,7 +447,7 @@ def __cmp__(self, other, trace=False): return result if trace: - print(f" all options evaluated: return 0") + print(" all options evaluated: return 0") return 0 @@ -443,7 +455,7 @@ def __cmp__(self, other, trace=False): GemConstraint.to_string = lambda gc: f"{gc.op} {gc.version}" -def sort_constraints(constraints): +def sort_constraints(constraints: Iterable[GemVersion]) -> list[GemConstraint]: """ Return a sorted sequence of unique GemConstraints. """ @@ -456,7 +468,7 @@ def sort_constraints(constraints): return consts -def tilde_comparator(version, requirement, trace=False): +def tilde_comparator(version: GemVersion, requirement: GemVersion, trace: bool = False) -> bool: """ Return True if ``version`` GemVersion satisfies ``requirement`` GemVersion according to the Rubygems tilde semantics. @@ -499,7 +511,7 @@ class GemRequirement: # The default requirement matches any version DEFAULT_CONSTRAINT = GemConstraint(">=", GemVersion(0)) - def __init__(self, *requirements): + def __init__(self, *requirements: str): """ Initialize a GemRequirement from a sequence of ``requirements`` converted to a constraints sequence of GemConstraint. @@ -509,16 +521,16 @@ def __init__(self, *requirements): else: self.constraints = tuple([GemRequirement.parse(r) for r in requirements]) - def __str__(self): + def __str__(self) -> str: gcs = [gc.to_string() for gc in sort_constraints(self.constraints)] return ", ".join(gcs) - def __repr__(self): + def __repr__(self) -> str: gcs = ", ".join(repr(gc.to_string()) for gc in sort_constraints(self.constraints)) return f"GemRequirement({gcs})" @classmethod - def from_string(cls, requirements): + def from_string(cls, requirements: str): """ Return a GemRequirement build from a lockfile-style ``requirements`` string. @@ -532,7 +544,7 @@ def from_string(cls, requirements): reqs = [r.strip() for r in reqs.split(",")] return cls(*reqs) - def for_lockfile(self): + def for_lockfile(self) -> str: """ Return a string representing this list of requirements suitable for use in a lockfile. @@ -546,13 +558,13 @@ def for_lockfile(self): gcs = ", ".join(gcs) return f" ({gcs})" - def dedupe(self): + def dedupe(self) -> Self: """ Return a new GemRequirement with sorted and unique constraints. """ return GemRequirement(*sort_constraints(self.constraints)) - def simplify(self): + def simplify(self) -> Self: """ Return a new simplified GemRequirement with: - sorted and unique constraints. @@ -567,7 +579,7 @@ def simplify(self): constraints.append(const) return GemRequirement(*sort_constraints(constraints)) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if not isinstance(other, self.__class__): return False @@ -589,7 +601,7 @@ def __eq__(self, other): return True return False - def exact(self): + def exact(self) -> bool: """ Return True if the requirement is for only an exact version. @@ -604,7 +616,7 @@ def exact(self): return len(self.constraints) == 1 and self.constraints[0].op == "=" @classmethod - def create(cls, reqs): + def create(cls, reqs: str | Iterable[str]) -> Self: """ Return a GemRequirement built from a single requirement string or a list of requirement strings. @@ -615,7 +627,7 @@ def create(cls, reqs): return cls(reqs) @classmethod - def parse(cls, requirement): + def parse(cls, requirement: str) -> GemConstraint: """ Return a GemConstraint tuple of (operator string, GemVersion object) parsed from a single ``requirement`` string such as "> 3.0". Also @@ -641,7 +653,7 @@ def parse(cls, requirement): op = match.group(1) if match.group(1) else "=" return GemConstraint(op, GemVersion(match.group(2))) - def satisfied_by(self, version, trace=False): + def satisfied_by(self, version: str | GemVersion, trace: bool = False) -> bool: """ Return True if the ``version`` GemVersion or version string or int satisfies all the constraints of this requirement. Raise an @@ -674,7 +686,7 @@ def satisfied_by(self, version, trace=False): return True - def tilde_requirements(self): + def tilde_requirements(self) -> list[GemConstraint]: """ Return a sorted sequence of all pessimistic "~>" GemConstraint. """ @@ -682,7 +694,7 @@ def tilde_requirements(self): return [gc for gc in constraints if gc.op == "~>"] -def get_tilde_constraints(constraint): +def get_tilde_constraints(constraint: GemConstraint) -> tuple[GemConstraint, GemConstraint]: """ Return a tuple of two GemConstraint representing the lower and upper bound of a version range ``string`` that uses a tilde "~>" pessimistic operator. diff --git a/src/univers/gentoo.py b/src/univers/gentoo.py index 543f8548..6d4f2a9e 100644 --- a/src/univers/gentoo.py +++ b/src/univers/gentoo.py @@ -5,6 +5,8 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + import re from univers.utils import cmp @@ -25,12 +27,12 @@ """ -def is_valid(string): +def is_valid(string: str) -> re.Match | None: version, _ = parse_version_and_revision(remove_spaces(string)) return _is_gentoo_version(version) -def parse_version_and_revision(version_string): +def parse_version_and_revision(version_string: str) -> tuple[str, int]: """ Return a tuple of (version string, revision int) given a ``version_string``. """ @@ -44,7 +46,7 @@ def parse_version_and_revision(version_string): return version, revision -def vercmp(ver1, ver2): +def vercmp(ver1: str, ver2: str) -> int: """ Compare two versions ``ver1`` and ``ver2`` and return 0, 1, or -1 according to the Python 2 cmp() semantics: diff --git a/src/univers/maven.py b/src/univers/maven.py index 368437a4..1b646c64 100644 --- a/src/univers/maven.py +++ b/src/univers/maven.py @@ -6,11 +6,24 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + import functools from itertools import zip_longest +from typing import TYPE_CHECKING from univers.utils import cmp +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Any + from typing import Final + + try: + from typing import Self + except ImportError: + from typing_extensions import Self + # TODO: Use common exceptions with other modules class VersionRangeParseError(ValueError): @@ -21,23 +34,23 @@ class RestrictionParseError(ValueError): pass -EXCLUSIVE_CLOSE = ")" -EXCLUSIVE_OPEN = "(" -INCLUSIVE_CLOSE = "]" -INCLUSIVE_OPEN = "[" +EXCLUSIVE_CLOSE: Final[str] = ")" +EXCLUSIVE_OPEN: Final[str] = "(" +INCLUSIVE_CLOSE: Final[str] = "]" +INCLUSIVE_OPEN: Final[str] = "[" # Known qualifiers, oldest to newest -QUALIFIERS = ["alpha", "beta", "milestone", "rc", "snapshot", "", "sp"] +QUALIFIERS: Final[list[str]] = ["alpha", "beta", "milestone", "rc", "snapshot", "", "sp"] # Well defined aliases -ALIASES = { +ALIASES: Final[dict[str, str]] = { "ga": "", "final": "", "cr": "rc", } -def list2tuple(l): +def list2tuple(l: Iterable[Any]) -> tuple[Any, ...]: return tuple(list2tuple(x) if isinstance(x, list) else x for x in l) @@ -45,7 +58,7 @@ def list2tuple(l): class Restriction(object): """Describes a restriction in versioning""" - def __init__(self, spec=None): + def __init__(self, spec: str | None = None): """Create a restriction Restrictions are specified using a semi-mathematical notation: @@ -93,7 +106,7 @@ def __init__(self, spec=None): self.lower_bound = version self.upper_bound = version - def __contains__(self, version): + def __contains__(self, version: Version) -> bool: """Return true if version is contained within the restriction version must be greater than the lower bound (or equal to it if the @@ -116,7 +129,7 @@ def __contains__(self, version): return True - def __cmp__(self, other): + def __cmp__(self, other: Any) -> int: if self is other: return 0 @@ -134,10 +147,10 @@ def __cmp__(self, other): result = cmp(self.upper_bound_inclusive, other.upper_bound_inclusive) return result - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return self.__cmp__(other) == 0 - def __hash__(self): + def __hash__(self) -> int: return hash( ( self.lower_bound, @@ -147,13 +160,13 @@ def __hash__(self): ) ) - def __lt__(self, other): + def __lt__(self, other: Any) -> bool: return self.__cmp__(other) < 0 - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: return self.__cmp__(other) != 0 - def __str__(self): + def __str__(self) -> str: s = "{open}{lower}{comma}{upper}{close}".format( open=(INCLUSIVE_OPEN if self.lower_bound_inclusive else EXCLUSIVE_OPEN), lower=self.lower_bound if self.lower_bound is not None else "", @@ -167,7 +180,7 @@ def __str__(self): ) return s - def __repr__(self): + def __repr__(self) -> str: return "<%s.%s(%r, %r, %r, %r)>" % ( self.__module__, "Restriction", @@ -178,7 +191,7 @@ def __repr__(self): ) @classmethod - def fromstring(cls, spec): + def fromstring(cls, spec: str) -> Self: return cls(spec) @@ -189,7 +202,7 @@ class VersionRange(object): Valid ranges are comma separated range specifications """ - def __init__(self, spec): + def __init__(self, spec: str): """Create a VersionRange from a string specification :param spec string representation of a version or version range @@ -245,7 +258,7 @@ def __init__(self, spec): self.version = version self.restrictions = tuple(restrictions) - def __cmp__(self, other): + def __cmp__(self, other: Any) -> int: if self is other: return 0 @@ -262,28 +275,28 @@ def __cmp__(self, other): return result - def __contains__(self, version): + def __contains__(self, version: Version) -> bool: return any((version in r) for r in self.restrictions) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return self.__cmp__(other) == 0 - def __hash__(self): + def __hash__(self) -> int: return hash((self.version, self.restrictions)) - def __lt__(self, other): + def __lt__(self, other: Any) -> bool: return self.__cmp__(other) < 0 - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: return self.__cmp__(other) != 0 - def __str__(self): + def __str__(self) -> str: if self.version: return str(self.version) else: return ",".join(str(r) for r in self.restrictions) - def __repr__(self): + def __repr__(self) -> str: return "<%s.%s(%r, %r)>" % ( self.__module__, "VersionRange", @@ -291,7 +304,9 @@ def __repr__(self): self.restrictions, ) - def _intersection(self, l1, l2): + def _intersection( + self, l1: Iterable[Restriction], l2: Iterable[Restriction] + ) -> Iterable[Restriction]: """Return the intersection of l1 and l2 :param l1 list of restrictions @@ -304,14 +319,14 @@ def _intersection(self, l1, l2): raise NotImplementedError @classmethod - def fromstring(cls, spec): + def fromstring(cls, spec: str) -> Self: return cls(spec) @classmethod - def from_version(cls, version): + def from_version(cls, version: Version) -> Self: return cls(str(version)) - def restrict(self, version_range): + def restrict(self, version_range: VersionRange) -> VersionRange: """Returns a new VersionRange that is a restriction of this and the specified version range. @@ -324,7 +339,7 @@ def restrict(self, version_range): """ raise NotImplementedError - def match_version(self, versions): + def match_version(self, versions: Iterable[Version]) -> Version | None: matched = None for version in sorted(versions, reverse=True): if version in self: @@ -335,9 +350,9 @@ def match_version(self, versions): @functools.total_ordering class Version(object): - """Maven version objecjt""" + """Maven version object""" - def __init__(self, version): + def __init__(self, version: str): """Create a maven version The version string is examined one character at a time. @@ -402,7 +417,7 @@ def __init__(self, version): self._parsed = list2tuple(self._normalize(parsed)) - def __cmp__(self, other): + def __cmp__(self, other: str | VersionRange | Self) -> int: if self is other: return 0 @@ -415,25 +430,29 @@ def __cmp__(self, other): return self._compare(self._parsed, other._parsed) - def __eq__(self, other): + def __eq__(self, other: str | VersionRange | Self) -> bool: return self.__cmp__(other) == 0 - def __hash__(self): + def __hash__(self) -> int: return hash(self._unparsed) - def __lt__(self, other): + def __lt__(self, other: str | VersionRange | Self) -> bool: return self.__cmp__(other) < 0 - def __ne__(self, other): + def __ne__(self, other: str | VersionRange | Self) -> bool: return self.__cmp__(other) != 0 - def __repr__(self): + def __repr__(self) -> str: return "<%s.%s(%r)>" % (self.__module__, "Version", self._unparsed) - def __str__(self): + def __str__(self) -> str: return self._unparsed - def _compare(self, this, other): + def _compare( + self, + this: int | str | list[int | str] | tuple[int | str, ...], + other: int | str | list[int | str] | tuple[int | str, ...], + ) -> int: if isinstance(this, int): return self._int_compare(this, other) elif isinstance(this, str): @@ -443,7 +462,7 @@ def _compare(self, this, other): else: raise RuntimeError("Unknown type for t: %r" % this) - def _int_compare(self, this, other): + def _int_compare(self, this: int, other: int | str | list[Any] | tuple[Any, ...] | None) -> int: if isinstance(other, int): return this - other elif isinstance(other, (str, list, tuple)): @@ -453,7 +472,9 @@ def _int_compare(self, this, other): else: raise RuntimeError("other is of invalid type: %s" % type(other)) - def _list_compare(self, l, other): + def _list_compare( + self, l: Iterable[Any], other: int | str | None | list[None | int | str] + ) -> int: if other is None: if len(l) == 0: return 0 @@ -478,7 +499,7 @@ def _list_compare(self, l, other): else: raise RuntimeError("other is of invalid type: %s" % type(other)) - def _new_list(self, l): + def _new_list(self, l: list[Any]) -> list[Any]: """Create a new sublist, append it to the current list and return the sublist @@ -491,7 +512,7 @@ def _new_list(self, l): l.append(sublist) return sublist - def _normalize(self, l): + def _normalize(self, l: list[Any]) -> list[Any]: for item in l[::-1]: if not item: l.pop() @@ -499,7 +520,7 @@ def _normalize(self, l): break return l - def _string_compare(self, s, other): + def _string_compare(self, s: str, other: str | int | None | list[Any] | tuple[Any, ...]) -> int: """Compare string item `s` to `other` :param str s: string item to compare @@ -523,7 +544,7 @@ def _string_compare(self, s, other): else: raise RuntimeError("other is of invalid type: %s" % type(other)) - def _parse_buffer(self, buf, followed_by_digit=False): + def _parse_buffer(self, buf: str, followed_by_digit: bool = False) -> str: """Parse the string buf to determine if it is string or an int :param str buf: string to parse @@ -543,7 +564,7 @@ def _parse_buffer(self, buf, followed_by_digit=False): return ALIASES.get(buf, buf) - def _string_value(self, s): + def _string_value(self, s: str) -> str: """Convert a string into a comparable value. If the string is a known qualifier, or an alias of a known qualifier, @@ -560,5 +581,5 @@ def _string_value(self, s): return "%d-%s" % (len(QUALIFIERS), s) @classmethod - def fromstring(cls, spec): + def fromstring(cls, spec: str) -> Self: return cls(spec) diff --git a/src/univers/nuget.py b/src/univers/nuget.py index a5cd8678..4953dfc8 100644 --- a/src/univers/nuget.py +++ b/src/univers/nuget.py @@ -6,16 +6,30 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + import functools import re +from typing import TYPE_CHECKING import semver -_PAD_WIDTH = 8 -_FAKE_PRE_WIDTH = 16 +if TYPE_CHECKING: + from typing import Any + from typing import Final + from typing import SupportsInt + + try: + from typing import Self + except ImportError: + from typing_extensions import Self + + +_PAD_WIDTH: Final[int] = 8 +_FAKE_PRE_WIDTH: Final[int] = 16 -def _strip_leading_v(version): +def _strip_leading_v(version: str) -> str: """Strip leading v from the version, if any.""" # Versions starting with "v" aren't valid SemVer, but we handle them just in # case. @@ -25,7 +39,7 @@ def _strip_leading_v(version): return version -def _remove_leading_zero(component): +def _remove_leading_zero(component: str) -> str: """Remove leading zeros from a component.""" if component[0] == ".": return "." + str(int(component[1:])) @@ -33,7 +47,7 @@ def _remove_leading_zero(component): return str(int(component)) -def coerce(version): +def coerce(version: str) -> str: """Coerce a potentially invalid semver into valid semver.""" version = _strip_leading_v(version) version_pattern = re.compile(r"^(\d+)(\.\d+)?(\.\d+)?(.*)$") @@ -49,17 +63,17 @@ def coerce(version): ) -def is_valid(version): +def is_valid(version: str) -> bool: """Returns whether or not the version is a valid semver.""" return semver.VersionInfo.isvalid(_strip_leading_v(version)) -def parse(version): +def parse(version: str) -> semver.VersionInfo: """Parse a SemVer.""" return semver.VersionInfo.parse(coerce(version)) -def normalize(version): +def normalize(version: str) -> str: """Normalize semver version for indexing (to allow for lexical sorting/filtering).""" version = parse(version) @@ -122,7 +136,7 @@ def normalize(version): return f"{core_parts}-{pre}" -def _extract_revision(str_version): +def _extract_revision(str_version: str) -> tuple[str, int]: """ Extract revision from ``str_version`` and return a tuple of: (dotted version string without revision, revision integer). @@ -149,7 +163,7 @@ class InvalidNuGetVersion(Exception): class Version: """NuGet version.""" - def __init__(self, base_semver, revision=0): + def __init__(self, base_semver: semver.VersionInfo, revision: int = 0): self._base_semver = base_semver if self._base_semver.prerelease: # TODO: why lowercasing this and not the build and why here and now? @@ -157,14 +171,14 @@ def __init__(self, base_semver, revision=0): self._revision = revision or 0 self._original_version = None - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return ( isinstance(other, Version) and self._base_semver == other._base_semver and self._revision == other._revision ) - def __lt__(self, other): + def __lt__(self, other: Any) -> bool: if not isinstance(other, Version): return NotImplemented if self._base_semver.replace(prerelease="") == other._base_semver.replace(prerelease=""): @@ -175,7 +189,7 @@ def __lt__(self, other): # Revision is the same, so ignore it for comparison purposes. return self._base_semver < other._base_semver - def __hash__(self): + def __hash__(self) -> int: return hash( ( self._base_semver.to_tuple(), @@ -184,7 +198,7 @@ def __hash__(self): ) @classmethod - def from_string(cls, str_version): + def from_string(cls, str_version: str) -> Self: if not str_version: return @@ -202,10 +216,15 @@ def from_string(cls, str_version): vers._original_version = original return vers - def __repr__(self): + def __repr__(self) -> str: return f"Version<{self.to_string()}>" - def to_string(self, with_empty_revision=False, include_prerelease=True, include_build=True): + def to_string( + self, + with_empty_revision: bool = False, + include_prerelease: bool = True, + include_build: bool = True, + ) -> str: major = self.major or "0" minor = self.minor or "0" patch = self.patch or "0" @@ -226,13 +245,13 @@ def to_string(self, with_empty_revision=False, include_prerelease=True, include_ return f"{major}.{minor}.{patch}{revision}{prerelease}{build}" - def __str__(self): + def __str__(self) -> str: return self.to_string( with_empty_revision=False, include_prerelease=True, include_build=True ) @property - def base_version(self): + def base_version(self) -> str: """ Return the base version dotted string composed of the four numerical segments including revision and ignoring prerelease and build. @@ -242,25 +261,25 @@ def base_version(self): ) @property - def major(self): + def major(self) -> SupportsInt: return self._base_semver.major @property - def minor(self): + def minor(self) -> SupportsInt: return self._base_semver.minor @property - def patch(self): + def patch(self) -> SupportsInt: return self._base_semver.patch @property - def revision(self): + def revision(self) -> SupportsInt: return self._revision @property - def prerelease(self): + def prerelease(self) -> str: return self._base_semver.prerelease and self._base_semver.prerelease or "" @property - def build(self): + def build(self) -> str: return self._base_semver.build and self._base_semver.build or "" diff --git a/src/univers/py.typed b/src/univers/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/univers/rpm.py b/src/univers/rpm.py index f597aca8..f34c7ab9 100644 --- a/src/univers/rpm.py +++ b/src/univers/rpm.py @@ -9,9 +9,17 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + import re +from typing import TYPE_CHECKING from typing import NamedTuple -from typing import Union + +if TYPE_CHECKING: + try: + from typing import Self + except ImportError: + from typing_extensions import Self class RpmVersion(NamedTuple): @@ -23,10 +31,10 @@ class RpmVersion(NamedTuple): version: str release: str - def __str__(self, *args, **kwargs): + def __str__(self, *args, **kwargs) -> str: return self.to_string() - def to_string(self): + def to_string(self) -> str: if self.release: vr = f"{self.version}-{self.release}" else: @@ -37,28 +45,28 @@ def to_string(self): return vr @classmethod - def from_string(cls, s): + def from_string(cls, s: str) -> Self: s.strip() e, v, r = from_evr(s) return cls(e, v, r) - def __lt__(self, other): + def __lt__(self, other: Self | str) -> bool: return compare_rpm_versions(self, other) < 0 - def __gt__(self, other): + def __gt__(self, other: Self | str) -> bool: return compare_rpm_versions(self, other) > 0 - def __eq__(self, other): + def __eq__(self, other: Self | str) -> bool: return compare_rpm_versions(self, other) == 0 - def __le__(self, other): + def __le__(self, other: Self | str) -> bool: return compare_rpm_versions(self, other) <= 0 - def __ge__(self, other): + def __ge__(self, other: Self | str) -> bool: return compare_rpm_versions(self, other) >= 0 -def from_evr(s): +def from_evr(s: str) -> tuple[int, str, str]: """ Return an (E, V, R) tuple given a string by splitting [e:]version-release into the three possible subcomponents. @@ -83,7 +91,7 @@ def from_evr(s): return e, v, r -def compare_rpm_versions(a: Union[RpmVersion, str], b: Union[RpmVersion, str]) -> int: +def compare_rpm_versions(a: RpmVersion | str, b: RpmVersion | str) -> int: """ Compare two RPM versions ``a`` and ``b`` and return: - 1 if the version of a is newer than b @@ -136,7 +144,7 @@ class Vercmp: R_ALPHA = re.compile(rb"^([a-zA-Z]+)(.*)$") @classmethod - def compare(cls, first, second): + def compare(cls, first: str, second: str) -> int: # Rpm versions can only be ascii, anything else is just ignored first = first.encode("ascii", "ignore") second = second.encode("ascii", "ignore") @@ -236,5 +244,5 @@ def compare(cls, first, second): return -1 -def vercmp(first, second): +def vercmp(first: str, second: str) -> int: return Vercmp.compare(first, second) diff --git a/src/univers/univers_semver.py b/src/univers/univers_semver.py index d617b4aa..d3c6f053 100644 --- a/src/univers/univers_semver.py +++ b/src/univers/univers_semver.py @@ -4,6 +4,8 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + from univers.utils import remove_spaces from univers.version_constraint import VersionConstraint from univers.versions import SemverVersion @@ -13,7 +15,7 @@ """ -def get_caret_constraints(string): +def get_caret_constraints(string: str) -> tuple[VersionConstraint, VersionConstraint]: """ Return a tuple of two VersionConstraint of ``SemverVersion`` representing the lower and upper bound of version constraint ``string`` that contains a @@ -41,7 +43,9 @@ def get_caret_constraints(string): ) -def get_tilde_constraints(string, operator="~"): +def get_tilde_constraints( + string: str, operator: str = "~" +) -> tuple[VersionConstraint, VersionConstraint]: """ Return a tuple of two VersionConstraint of ``SemverVersion`` representing the lower and upper bound of a version range ``string`` that contains a @@ -70,7 +74,7 @@ def get_tilde_constraints(string, operator="~"): # FIXME: this is unlikely correct https://github.com/npm/node-semver/issues/112 -def get_pessimistic_constraints(string): +def get_pessimistic_constraints(string: str) -> tuple[VersionConstraint, VersionConstraint]: """ Return a tuple of two VersionConstraint of ``SemverVersion`` representing the lower and upper bound of version range ``string`` that contains a diff --git a/src/univers/utils.py b/src/univers/utils.py index 63952ce4..6abcdfe3 100644 --- a/src/univers/utils.py +++ b/src/univers/utils.py @@ -4,12 +4,16 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations -def remove_spaces(string): +from typing import Any + + +def remove_spaces(string: str) -> str: return "".join(string.split()) -def cmp(x, y): +def cmp(x: Any, y: Any) -> int: """ Replacement for built-in Python 2 function cmp that was removed in Python 3 From https://docs.python.org/2/library/functions.html?highlight=cmp#cmp : diff --git a/src/univers/version_constraint.py b/src/univers/version_constraint.py index 36ec6d81..d54ebd7f 100644 --- a/src/univers/version_constraint.py +++ b/src/univers/version_constraint.py @@ -4,8 +4,17 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + import operator +from collections.abc import Iterable from functools import total_ordering +from typing import Any +from typing import Callable +from typing import Dict +from typing import List +from typing import Literal +from typing import Tuple import attr @@ -19,7 +28,7 @@ # back from docs at https://docs.python.org/3/library/itertools.html#itertools.pairwise import itertools - def pairwise(iterable): + def pairwise(iterable: Iterable[Any]) -> Iterable[Tuple[Any, Any]]: a, b = itertools.tee(iterable) next(b, None) return zip(a, b) @@ -32,7 +41,7 @@ def pairwise(iterable): """ -def operator_star(a, b): +def operator_star(a: Any, b: Any) -> Literal[True]: """ Comparison operator for the star "*" constraint comparator. Since it matches any version, it is always True. @@ -76,7 +85,7 @@ class VersionConstraint: # a Version subclass version_class = attr.ib(type=Version, default=None, repr=False) - def __attrs_post_init__(self): + def __attrs_post_init__(self) -> None: # Notes: setattr is used because this is an immutable frozen instance. # See https://www.attrs.org/en/stable/init.html?#post-init try: @@ -95,7 +104,7 @@ def __attrs_post_init__(self): else: raise ValueError("Cannot build a VersionConstraint without a version class") - def __str__(self): + def __str__(self) -> str: """ Return a string representing this constraint. For example:: @@ -116,27 +125,27 @@ def __str__(self): to_string = __str__ - def to_dict(self): + def to_dict(self) -> Dict[str, str]: return dict(comparator=self.comparator, version=str(self.version)) - def __hash__(self): + def __hash__(self) -> int: return hash(str(self)) - def __eq__(self, other): + def __eq__(self, other: "VersionConstraint") -> bool: if not isinstance(other, self.__class__): return NotImplemented return self.comparator == other.comparator and self.version == other.version - def __lt__(self, other): + def __lt__(self, other: "VersionConstraint") -> bool: if not isinstance(other, self.__class__): return NotImplemented # we compare tuples, version first return (self.version, self.comparator).__lt__((other.version, other.comparator)) - def is_star(self): + def is_star(self) -> bool: return self.comparator == "*" - def invert(self): + def invert(self) -> "VersionConstraint": """ Return a new VersionConstraint instance with the comparator inverted. For example:: @@ -161,7 +170,7 @@ def invert(self): ) @classmethod - def from_string(cls, string, version_class): + def from_string(cls, string: str, version_class: Version) -> "VersionConstraint": """ Return a single VersionConstraint built from a constraint ``string`` and a ``version_class`` Version class. @@ -188,7 +197,7 @@ def from_string(cls, string, version_class): return cls(comparator=comparator, version=version, version_class=version_class) @staticmethod - def split(string): + def split(string: str) -> Tuple[str, str]: """ Return a tuple of (comparator, version) strings given a constraint ``string`` such as ">=2.3". @@ -220,7 +229,7 @@ def split(string): # default to equality return "=", constraint_string - def __contains__(self, version): + def __contains__(self, version: Version) -> bool: """ Return a True if the ``version`` Version is contained in this VersionConstraint or "satisfies" this VersionConstraint. @@ -262,7 +271,7 @@ def __contains__(self, version): contains = __contains__ @classmethod - def validate(cls, constraints): + def validate(cls, constraints: List["VersionConstraint"] | Tuple["VersionConstraint"]): """ Raise an assertion error if the ``constraints`` is not a list of VersionConstraint objects or if two VersionConstraint contain the same @@ -292,7 +301,7 @@ def validate(cls, constraints): return validate_comparators(constraints) @classmethod - def simplify(cls, constraints): + def simplify(cls, constraints: Iterable["VersionConstraint"]) -> List["VersionConstraint"]: """ Return a new simplified ``constraints`` list with duplicated constraints removed. This includes removing exact duplicates and redundant @@ -303,7 +312,7 @@ def simplify(cls, constraints): return constraints -def deduplicate(constraints): +def deduplicate(constraints: Iterable[Any]) -> List[Any]: """ Return a new ``constraints`` list with exact duplicated constraints removed. """ @@ -316,7 +325,7 @@ def deduplicate(constraints): return unique -def validate_comparators(constraints): +def validate_comparators(constraints: Iterable[VersionConstraint]) -> bool: """ Raise an assertion error if the ``constraints`` list contains an invalid sequence of constraint comparators according to ``vers`` rules. @@ -399,7 +408,7 @@ def validate_comparators(constraints): return True -def simplify_constraints(constraints): +def simplify_constraints(constraints: Iterable[VersionConstraint]) -> List[VersionConstraint]: """ Return a list of VersionConstraint given a ``constraints`` list by discarding redundant constraints according to ``vers`` rules. @@ -475,7 +484,7 @@ class InvalidConstraintsError(Exception): pass -def contains_version(version, constraints): +def contains_version(version: Version, constraints: Iterable[VersionConstraint]) -> bool: """ Return True an assertion error if the ``constraints`` list contains the ``version`` Version object according to ``vers`` rules. diff --git a/src/univers/version_range.py b/src/univers/version_range.py index bee601ef..00670abe 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -4,7 +4,9 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. -from typing import List +from __future__ import annotations + +from typing import TYPE_CHECKING from typing import Union import attr @@ -24,6 +26,17 @@ from univers.versions import AllVersion from univers.versions import NoneVersion +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Any + from typing import Final + from typing import TypeAlias + + try: + from typing import Self + except ImportError: + from typing_extensions import Self + class InvalidVersionRange(Exception): """ @@ -31,7 +44,7 @@ class InvalidVersionRange(Exception): """ -INVERTED_COMPARATORS = { +INVERTED_COMPARATORS: Final[dict[str, str]] = { ">=": "<", "<=": ">", "!=": "=", @@ -66,39 +79,41 @@ class VersionRange: # timeline constraints = attr.ib(type=tuple, default=attr.Factory(tuple)) - def __attrs_post_init__(self, *args, **kwargs): + def __attrs_post_init__(self, *args, **kwargs) -> None: constraints = tuple(sorted(self.constraints)) # Notes: setattr is used because this is an immutable frozen instance. # See https://www.attrs.org/en/stable/init.html?#post-init object.__setattr__(self, "constraints", constraints) @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: """ Return a VersionRange built from a scheme-specific, native version range ``string``. Subclasses can implement. """ - return NotImplementedError + raise NotImplementedError @classmethod - def from_natives(cls, strings): + def from_natives(cls, strings: str | Iterable[str]) -> Self: """ Return a VersionRange built from a ``strings`` list of scheme- specific native version range strings. Subclasses can implement. """ - return NotImplementedError + raise NotImplementedError - def to_native(self, *args, **kwargs): + def to_native(self, *args, **kwargs) -> Any: """ Return a native range string for this VersionRange. Subclasses can implement. Optional ``args`` and ``kwargs`` allow subclass to require extra arguments (such as a package name that some scheme may require like for deb and rpm.) """ - return NotImplementedError + raise NotImplementedError @classmethod - def from_string(cls, vers, simplify=False, validate=False): + def from_string( + cls, vers: str, simplify: bool = False, validate: bool = False + ) -> RangeClassType: """ Return a VersionRange built from a ``vers`` version range spec string, such as "vers:npm/1.2.3,>=2.0.0" @@ -170,13 +185,13 @@ def from_string(cls, vers, simplify=False, validate=False): return range_class(parsed_constraints) @classmethod - def from_versions(cls, sequence): + def from_versions(cls, sequence: Iterable[str]) -> Self: """ Return a VersionRange built from a list of version strings, such as ["3.0.0", "1.0.1b", "3.0.2", "0.9.7a", "1.1.1ka"] """ if not cls.scheme or not cls.version_class: - return NotImplementedError + raise NotImplementedError constraints = [] for version in sequence: @@ -185,10 +200,10 @@ def from_versions(cls, sequence): constraints.append(constraint) return cls(constraints=constraints) - def is_star(self): + def is_star(self) -> bool: return len(self.constraints) == 1 and self.constraints[0].is_star() - def invert(self): + def invert(self) -> Self: """ Return the inverse or complement of this VersionRange. For example, if this range is ">=1.0.0", the inverse is "<1.0.0". @@ -206,17 +221,17 @@ def invert(self): return self.__class__(constraints=inverted_constraints) - def __str__(self): + def __str__(self) -> str: constraints = "|".join(str(c) for c in sorted(self.constraints)) return f"vers:{self.scheme}/{constraints}" to_string = __str__ - def to_dict(self): + def to_dict(self) -> dict[str, Any]: constraints = [c.to_dict() for c in self.constraints] return dict(scheme=self.scheme, constraints=constraints) - def __contains__(self, version): + def __contains__(self, version: versions.Version) -> bool: """ Return True if this VersionRange contains the ``version`` Version object. A version is contained in a VersionRange if it satisfies its @@ -237,14 +252,14 @@ def __contains__(self, version): contains = __contains__ - def __eq__(self, other): + def __eq__(self, other: Self) -> bool: return ( self.scheme == other.scheme and self.version_class == other.version_class and self.constraints == other.constraints ) - def normalize(self, known_versions: List[str]): + def normalize(self, known_versions: list[str]) -> Self: """ Return a new VersionRange normalized and simplified using the universe of ``known_versions`` list of version strings. @@ -276,14 +291,14 @@ def normalize(self, known_versions: List[str]): return self.__class__(constraints=version_constraints) -def from_cve_v4(data, scheme): +def from_cve_v4(data: dict[str, Any], scheme: str) -> None: """ Return a VersionRange build from the provided CVE V4 API ``data`` using the provided versioning vers ``scheme``. """ -def from_cve_v5(data, scheme): +def from_cve_v5(data: dict[str, Any], scheme: str) -> None: """ Return a VersionRange build from the provided CVE V5 API ``data`` using the provided versioning vers ``scheme``. @@ -296,14 +311,14 @@ def from_cve_v5(data, scheme): """ -def from_osv_v1(data, scheme): +def from_osv_v1(data: dict[str, Any], scheme: str) -> None: """ Return a VersionRange build from the provided CVE V4 API data using the provided versioning vers ``scheme``. """ -def get_allof_constraints(cls, clause): +def get_allof_constraints(cls: VersionRange, clause: AllOf) -> list[VersionConstraint]: """ Return a list of VersionConstraint given an AllOf ``clause``. """ @@ -318,7 +333,9 @@ def get_allof_constraints(cls, clause): return allof_constraints -def get_npm_version_constraints_from_semver_npm_spec(string, cls): +def get_npm_version_constraints_from_semver_npm_spec( + string: str, cls: VersionRange +) -> list[VersionConstraint]: """ Return a VersionConstraint for the provided ``string``. """ @@ -341,7 +358,7 @@ class NpmVersionRange(VersionRange): scheme = "npm" version_class = versions.SemverVersion - vers_by_native_comparators = { + vers_by_native_comparators: Final[dict[str, str]] = { "==": "=", "<=": "<=", ">=": ">=", @@ -351,7 +368,7 @@ class NpmVersionRange(VersionRange): } @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: """ Return a VersionRange built from an npm "node-semver" range ``string``. """ @@ -442,11 +459,11 @@ def from_native(cls, string): class ConanVersionRange(VersionRange): - scheme = "conan" + scheme: str = "conan" version_class = versions.ConanVersion @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: """ Return a VersionRange built from a conan range ``string``. """ @@ -476,10 +493,10 @@ class GemVersionRange(VersionRange): See https://github.com/npm/node-semver/issues/112 """ - scheme = "gem" + scheme: str = "gem" version_class = versions.RubygemsVersion - vers_by_native_comparators = { + vers_by_native_comparators: dict[str, str] = { "=": "=", "!=": "!=", "<=": "<=", @@ -489,7 +506,7 @@ class GemVersionRange(VersionRange): } @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: """ Return a VersionRange built from a Rubygem version range ``string``. @@ -509,7 +526,7 @@ def from_native(cls, string): return cls(constraints=constraints) -def split_req(string, comparators, default=None, strip=""): +def split_req(string: str, comparators: dict[str, str], default=None, strip="") -> tuple[str, str]: """ Return a tuple of (vers comparator, version) strings given an common version requirement``string`` such as "> 2.3" or "<= 2.3" using the ``comparators`` @@ -572,7 +589,7 @@ class DebianVersionRange(VersionRange): scheme = "deb" version_class = versions.DebianVersion - vers_by_native_comparators = { + vers_by_native_comparators: Final[dict[str, str]] = { "=": "=", "<=": "<=", ">=": ">=", @@ -584,7 +601,7 @@ class DebianVersionRange(VersionRange): } @classmethod - def split(cls, string): + def split(cls, string: str) -> tuple[str, str]: """ Return a tuple of (vers comparator, version) strings given a Debian version relationship ``string`` such as ">>2.3" or "(<< 2.3)". Raise a @@ -613,7 +630,7 @@ def split(cls, string): ) @classmethod - def build_constraint_from_string(cls, string): + def build_constraint_from_string(cls, string: str) -> VersionConstraint: """ Return a VersionConstraint built from a single Debian version relationship ``string``. @@ -630,7 +647,7 @@ def build_constraint_from_string(cls, string): return VersionConstraint(comparator=comparator, version=version) @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: """ Return a VersionRange built from a ``string`` single Debian version relationship string. @@ -643,7 +660,7 @@ def from_native(cls, string): return cls(constraints=[cls.build_constraint_from_string(string)]) @classmethod - def from_natives(cls, strings): + def from_natives(cls, strings: str | Iterable[str]) -> Self: """ Return a VersionRange built from a ``strings`` list of Debian version relationships or a single relationship string. @@ -699,7 +716,7 @@ class PypiVersionRange(VersionRange): scheme = "pypi" version_class = versions.PypiVersion - vers_by_native_comparators = { + vers_by_native_comparators: Final[dict[str, str]] = { # 01.01.01 is equal 1.1.1 e.g., with version normalization "==": "=", "!=": "!=", @@ -718,7 +735,7 @@ class PypiVersionRange(VersionRange): } @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: """ Return a VersionRange built from a PyPI PEP440 version specifiers ``string``. Raise a univers.versions.InvalidVersion @@ -783,7 +800,7 @@ class MavenVersionRange(VersionRange): version_class = versions.MavenVersion @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: """ Return a VersionRange built from a Maven version specifier ``string``. """ @@ -832,7 +849,7 @@ def from_native(cls, string): return cls(constraints=constraints) @classmethod - def from_natives(cls, strings): + def from_natives(cls, strings: str | Iterable[str]) -> Self: if isinstance(strings, str): return cls.from_native(strings) constraints = [] @@ -856,7 +873,7 @@ class ComposerVersionRange(VersionRange): scheme = "composer" version_class = versions.ComposerVersion - vers_by_native_comparators = { + vers_by_native_comparators: Final[dict[str, str]] = { "==": "=", "<=": "<=", ">=": ">=", @@ -872,7 +889,7 @@ class RpmVersionRange(VersionRange): scheme = "rpm" version_class = versions.RpmVersion - vers_by_native_comparators = { + vers_by_native_comparators: Final[dict[str, str]] = { "=": "=", "<=": "<=", ">=": ">=", @@ -886,7 +903,7 @@ class RpmVersionRange(VersionRange): } @classmethod - def build_constraint_from_string(cls, string): + def build_constraint_from_string(cls, string: str) -> VersionConstraint: """ Return a VersionConstraint built from a single RPM version relationship ``string``. @@ -907,7 +924,7 @@ def build_constraint_from_string(cls, string): return VersionConstraint(comparator=comparator, version=version) @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: """ Return a VersionRange built from a ``string`` single RPM version requirement string. @@ -920,7 +937,7 @@ def from_native(cls, string): return cls(constraints=[cls.build_constraint_from_string(string)]) @classmethod - def from_natives(cls, strings): + def from_natives(cls, strings: str | Iterable[str]) -> Self: """ Return a VersionRange built from a ``strings`` list of RPM version requirements or a single requirement string. @@ -958,7 +975,7 @@ class GolangVersionRange(VersionRange): scheme = "golang" version_class = versions.GolangVersion - vers_by_native_comparators = { + vers_by_native_comparators: Final[dict[str, str]] = { "==": "=", "<=": "<=", ">=": ">=", @@ -1068,7 +1085,7 @@ class NginxVersionRange(VersionRange): scheme = "nginx" version_class = versions.NginxVersion - vers_by_native_comparators = { + vers_by_native_comparators: Final[dict[str, str]] = { "==": "=", "<=": "<=", ">=": ">=", @@ -1077,7 +1094,7 @@ class NginxVersionRange(VersionRange): } @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: """ Return a VersionRange built from an nginx range ``string``. @@ -1171,7 +1188,7 @@ class OpensslVersionRange(VersionRange): version_class = versions.OpensslVersion @classmethod - def from_native(cls, string): + def from_native(cls, string: str) -> Self: cleaned = remove_spaces(string).lower() constraints = [] for version in cleaned.split(","): @@ -1196,7 +1213,7 @@ class NoneVersionRange(VersionRange): version_class = versions.NoneVersion -def from_gitlab_native(gitlab_scheme, string): +def from_gitlab_native(gitlab_scheme: str, string: str) -> RangeClassType: purl_scheme = gitlab_scheme if gitlab_scheme not in PURL_TYPE_BY_GITLAB_SCHEME.values(): purl_scheme = PURL_TYPE_BY_GITLAB_SCHEME[gitlab_scheme] @@ -1256,7 +1273,7 @@ def from_gitlab_native(gitlab_scheme, string): return vrc(constraints=constraints) -vers_by_github_native_comparators = { +vers_by_github_native_comparators: Final[dict[str, str]] = { "=": "=", "!=": "!=", "<=": "<=", @@ -1266,7 +1283,7 @@ def from_gitlab_native(gitlab_scheme, string): } -def build_constraint_from_github_advisory_string(scheme: str, string: str): +def build_constraint_from_github_advisory_string(scheme: str, string: str) -> VersionConstraint: """ Return a VersionConstraint built from a single github-native version relationship ``string``. @@ -1287,7 +1304,9 @@ def build_constraint_from_github_advisory_string(scheme: str, string: str): return VersionConstraint(comparator=comparator, version=version) -def build_range_from_github_advisory_constraint(scheme: str, string: Union[str, List]): +def build_range_from_github_advisory_constraint( + scheme: str, string: str | Iterable[str] +) -> RangeClassType: """ Github has a special syntax for version ranges. For example: @@ -1323,7 +1342,7 @@ def build_range_from_github_advisory_constraint(scheme: str, string: Union[str, return vrc(constraints=constraints) -vers_by_snyk_native_comparators = { +vers_by_snyk_native_comparators: Final[dict[str, str]] = { "==": "=", "=": "=", "!=": "!=", @@ -1334,7 +1353,7 @@ def build_range_from_github_advisory_constraint(scheme: str, string: Union[str, } -def split_req_bracket_notation(string): +def split_req_bracket_notation(string: str) -> tuple[str, str]: """ Return a tuple of (vers comparator, version) strings given an bracket notation version requirement ``string`` such as "(2.3" or "3.9]" @@ -1362,7 +1381,9 @@ def split_req_bracket_notation(string): raise ValueError(f"Unknown comparator in version requirement: {string!r} ") -def build_range_from_snyk_advisory_string(scheme: str, string: Union[str, List]): +def build_range_from_snyk_advisory_string( + scheme: str, string: str | Iterable[str] +) -> RangeClassType: """ Return a VersionRange built from a ``string`` single or multiple snyk version relationship string. @@ -1383,7 +1404,7 @@ def build_range_from_snyk_advisory_string(scheme: str, string: Union[str, List]) >>> assert str(vr) == "vers:pypi/<=9.21" """ version_constraints = [] - vrc = RANGE_CLASS_BY_SCHEMES[scheme] + vrc: RangeClassType = RANGE_CLASS_BY_SCHEMES[scheme] if isinstance(string, str): string = [string] @@ -1416,7 +1437,32 @@ def build_range_from_snyk_advisory_string(scheme: str, string: Union[str, List]) return vrc(constraints=version_constraints) -RANGE_CLASS_BY_SCHEMES = { +RangeClassType: TypeAlias = Union[ + NpmVersionRange, + DebianVersionRange, + PypiVersionRange, + MavenVersionRange, + NugetVersionRange, + ComposerVersionRange, + GemVersionRange, + RpmVersionRange, + GolangVersionRange, + GenericVersionRange, + ApacheVersionRange, + HexVersionRange, + CargoVersionRange, + MozillaVersionRange, + GitHubVersionRange, + EbuildVersionRange, + ArchLinuxVersionRange, + NginxVersionRange, + OpensslVersionRange, + MattermostVersionRange, + ConanVersionRange, + AllVersionRange, + NoneVersionRange, +] +RANGE_CLASS_BY_SCHEMES: Final[dict[str, RangeClassType]] = { "npm": NpmVersionRange, "deb": DebianVersionRange, "pypi": PypiVersionRange, @@ -1442,7 +1488,7 @@ def build_range_from_snyk_advisory_string(scheme: str, string: Union[str, List]) "none": NoneVersionRange, } -PURL_TYPE_BY_GITLAB_SCHEME = { +PURL_TYPE_BY_GITLAB_SCHEME: Final[dict[str, str]] = { "gem": "gem", "go": "golang", "maven": "maven", diff --git a/src/univers/versions.py b/src/univers/versions.py index c06f4ff0..51919b7a 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -4,6 +4,10 @@ # # Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. +from __future__ import annotations + +from typing import TYPE_CHECKING + import attr import semantic_version from packaging import version as packaging_version @@ -18,6 +22,12 @@ from univers.conan.version import Version as conan_version from univers.utils import remove_spaces +if TYPE_CHECKING: + from typing import Any + + from univers.version_constraint import VersionConstraint + + """ Version classes encapsulating the details of each version syntax. For instance semver is a version syntax. Python and Debian use another syntax. @@ -34,7 +44,7 @@ class InvalidVersion(ValueError): pass -def is_valid_alpine_version(s): +def is_valid_alpine_version(s: str) -> bool: """ Return True is the string `s` is a valid Alpine version. We do not support yet version strings that start with @@ -81,7 +91,7 @@ class Version: # a comparable scheme-specific version object constructed from the version string value = attr.ib(default=None, repr=False, eq=True, order=True, hash=True) - def __attrs_post_init__(self): + def __attrs_post_init__(self) -> None: normalized_string = self.normalize(self.string) if not self.is_valid(normalized_string): raise InvalidVersion(f"{self.string!r} is not a valid {self.__class__!r}") @@ -95,7 +105,7 @@ def __attrs_post_init__(self): object.__setattr__(self, "value", value) @classmethod - def is_valid(cls, string): + def is_valid(cls, string) -> bool: """ Return True if the ``string`` is a valid version for its scheme or False if not valid. The empty string, None, False and 0 are considered invalid. @@ -104,7 +114,7 @@ def is_valid(cls, string): return bool(string) @classmethod - def normalize(cls, string): + def normalize(cls, string: str) -> str: """ Return a normalized version string from ``string ``. Subclass can override. """ @@ -112,7 +122,7 @@ def normalize(cls, string): return remove_spaces(string).lstrip("vV") @classmethod - def build_value(self, string): + def build_value(self, string: str) -> str: """ Return a wrapped version "value" object for a version ``string``. Subclasses can override. The default is a no-op and returns the string @@ -121,7 +131,7 @@ def build_value(self, string): """ return string - def satisfies(self, constraint): + def satisfies(self, constraint: VersionConstraint) -> bool: """ Return True if this Version satisfies the ``constraint`` VersionConstraint. Satisfying means that this version is "within" the @@ -129,25 +139,25 @@ def satisfies(self, constraint): """ return self in constraint - def __str__(self): + def __str__(self) -> str: return str(self.value) class AllVersion(Version): @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: return string == "vers:all/*" class NoneVersion(Version): @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: return string == "vers:none/*" class GenericVersion(Version): @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: # generic implementation ... # TODO: Should use # https://github.com/repology/libversion/blob/master/doc/ALGORITHM.md#core-algorithm @@ -168,14 +178,14 @@ class PypiVersion(Version): # TODO: use packvers and handle legacy versions @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> packaging_version.Version: """ Return a packaging.version.LegacyVersion or packaging.version.Version """ return packaging_version.Version(string) @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: try: # Note: we consider only modern pep440 versions as valid. legacy # will fail validation for now. @@ -187,7 +197,7 @@ def is_valid(cls, string): class EnhancedSemanticVersion(semantic_version.Version): @property - def precedence_key(self): + def precedence_key(self) -> tuple[Any, ...]: key = super(EnhancedSemanticVersion, self).precedence_key return key + (self.build or ()) @@ -198,11 +208,11 @@ class SemverVersion(Version): """ @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> EnhancedSemanticVersion | semantic_version.Version: return EnhancedSemanticVersion.coerce(string) @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: try: cls.build_value(string) return True @@ -229,26 +239,26 @@ def prerelease(self): def build(self): return self.value and self.value.build - def next_major(self): + def next_major(self) -> "SemverVersion": return self.value and SemverVersion(str(self.value.next_major())) - def next_minor(self): + def next_minor(self) -> "SemverVersion": return self.value and SemverVersion(str(self.value.next_minor())) - def next_patch(self): + def next_patch(self) -> "SemverVersion": return self.value and SemverVersion(str(self.value.next_patch())) -def is_even(s): +def is_even(s: str) -> bool: """ Return True if the string "s" is an even number and False if this is an odd number. For example: - >>> is_even(4) + >>> is_even("4") True - >>> is_even(123) + >>> is_even("123") False - >>> is_even(0) + >>> is_even("0") True """ return (int(s) % 2) == 0 @@ -260,7 +270,7 @@ class NginxVersion(SemverVersion): """ @property - def is_stable(self): + def is_stable(self) -> bool: """ True if this is a "stable "version """ @@ -275,36 +285,36 @@ class RubygemsVersion(Version): """ @classmethod - def build_value(cls, string): + def build_value(cls, string) -> gem.GemVersion: return gem.GemVersion(string) @classmethod - def is_valid(cls, string): + def is_valid(cls, string) -> bool: return gem.GemVersion.is_correct(string) class ArchLinuxVersion(Version): - def __eq__(self, other): + def __eq__(self, other: "ArchLinuxVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented return arch.vercmp(self.value, other.value) == 0 - def __lt__(self, other): + def __lt__(self, other: "ArchLinuxVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented return arch.vercmp(self.value, other.value) < 0 - def __gt__(self, other): + def __gt__(self, other: "ArchLinuxVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented return arch.vercmp(self.value, other.value) > 0 - def __le__(self, other): + def __le__(self, other: "ArchLinuxVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented return arch.vercmp(self.value, other.value) <= 0 - def __ge__(self, other): + def __ge__(self, other: "ArchLinuxVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented return arch.vercmp(self.value, other.value) >= 0 @@ -312,11 +322,11 @@ def __ge__(self, other): class DebianVersion(Version): @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> debian.Version: return debian.Version.from_string(string) @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: return debian.Version.is_valid(string) @@ -325,11 +335,11 @@ class MavenVersion(Version): # https://github.com/apache/maven/tree/master/maven-artifact/src/main/java/org/apache/maven/artifact/versioning @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> maven.Version: return maven.Version(string) @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: try: cls.build_value(string) return True @@ -342,11 +352,11 @@ class NugetVersion(Version): # See https://docs.microsoft.com/en-us/nuget/concepts/package-versioning @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> nuget.Version: return nuget.Version.from_string(string) @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: try: cls.build_value(string) return True @@ -360,26 +370,26 @@ class RpmVersion(Version): """ @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> rpm.RpmVersion: return rpm.RpmVersion.from_string(string) class GentooVersion(Version): @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: return gentoo.is_valid(string) - def __eq__(self, other): + def __eq__(self, other: "GentooVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented return gentoo.vercmp(self.value, other.value) == 0 - def __lt__(self, other): + def __lt__(self, other: "GentooVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented return gentoo.vercmp(self.value, other.value) == -1 - def __gt__(self, other): + def __gt__(self, other: "GentooVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented return gentoo.vercmp(self.value, other.value) == 1 @@ -387,19 +397,19 @@ def __gt__(self, other): class AlpineLinuxVersion(GentooVersion): @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: return is_valid_alpine_version(string) and gentoo.is_valid(string) class ComposerVersion(SemverVersion): @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> SemverVersion: return super().build_value(string.lstrip("vV")) class GolangVersion(SemverVersion): @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> SemverVersion: return super().build_value(string.lstrip("vV")) @@ -434,11 +444,11 @@ def __attrs_post_init__(self): object.__setattr__(self, "patch", patch) @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: return bool(cls.parse(string)) @classmethod - def parse(cls, string): + def parse(cls, string: str) -> bool | tuple[int, int, int, str]: """ Return a four-tuple of (major, minor, build, patch) version segments where major, minor, build are integers and patch is a string possibly empty. @@ -489,13 +499,13 @@ def parse(cls, string): return major, minor, build, patch @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> "LegacyOpensslVersion": return cls.parse(string) - def __str__(self): + def __str__(self) -> str: return f"{self.major}.{self.minor}.{self.build}{self.patch}" - def __lt__(self, other): + def __lt__(self, other: "LegacyOpensslVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented # Check if versions have the same base, and `one and only one` of them is a pre-release. @@ -505,7 +515,7 @@ def __lt__(self, other): return self.is_prerelease() return self.value.__lt__(other.value) - def __gt__(self, other): + def __gt__(self, other: "LegacyOpensslVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented if (self.major, self.minor, self.build) == (other.major, other.minor, other.build) and ( @@ -514,7 +524,7 @@ def __gt__(self, other): return other.is_prerelease() return self.value.__gt__(other.value) - def is_prerelease(self): + def is_prerelease(self) -> bool: return self.patch.startswith(("-beta", "-alpha")) @@ -537,11 +547,11 @@ class OpensslVersion(Version): """ @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: return cls.is_valid_new(string) or cls.is_valid_legacy(string) @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> LegacyOpensslVersion | SemverVersion: """ Return a wrapped version "value" object depending on whether version is legacy or semver. @@ -552,7 +562,7 @@ def build_value(cls, string): return SemverVersion(string) @classmethod - def is_valid_new(cls, string): + def is_valid_new(cls, string: str) -> bool | None: """ Check the validity of new Openssl Version. @@ -569,17 +579,17 @@ def is_valid_new(cls, string): return sem.major >= 3 @classmethod - def is_valid_legacy(cls, string): + def is_valid_legacy(cls, string: str) -> bool: return LegacyOpensslVersion.is_valid(string) - def __eq__(self, other): + def __eq__(self, other: "OpensslVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented if not isinstance(other.value, self.value.__class__): return NotImplemented return self.value.__eq__(other.value) - def __lt__(self, other): + def __lt__(self, other: "OpensslVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented if isinstance(other.value, self.value.__class__): @@ -587,7 +597,7 @@ def __lt__(self, other): # By construction legacy version is always behind Semver return isinstance(self.value, LegacyOpensslVersion) - def __gt__(self, other): + def __gt__(self, other: "OpensslVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented if isinstance(other.value, self.value.__class__): @@ -595,7 +605,7 @@ def __gt__(self, other): # By construction semver version is always ahead of legacy return isinstance(self.value, SemverVersion) - def __le__(self, other): + def __le__(self, other: "OpensslVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented if isinstance(other.value, self.value.__class__): @@ -603,7 +613,7 @@ def __le__(self, other): # version value are of diff type, then legacy one is always behind semver return isinstance(self.value, LegacyOpensslVersion) - def __ge__(self, other): + def __ge__(self, other: "OpensslVersion") -> bool: if not isinstance(other, self.__class__): return NotImplemented if isinstance(other.value, self.value.__class__): @@ -614,11 +624,11 @@ def __ge__(self, other): class ConanVersion(Version): @classmethod - def build_value(cls, string): + def build_value(cls, string: str) -> conan_version: return conan_version(string) @classmethod - def is_valid(cls, string): + def is_valid(cls, string: str) -> bool: try: cls.build_value(string) return True