From addd5c5699a04d07ae479438f33de7fb19b70632 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Fri, 8 Jul 2022 14:48:55 +0100 Subject: [PATCH 1/7] Reorder `Specifier` regex groups to check wildcard matches early This ensures that an `re.match` or `re.search` does not exclude the wildcard in cases where it would otherwise match. Without this reordering, a regex match of a wildcard specifier would only match the version sections before it since both parts of the prior part of the unnamed group are optional, allowing the group to match an empty string even though it is optional as a whole. This does not affect `Specifier` since the regex used by it gets a `$` added to it, forcing the regular expression engine to backtrack in such cases. It does, however, affect `Requirement` since it is currently allowing these values through since they match the regex for `LegacySpecifier`. --- packaging/specifiers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index a2d51b04..ec0d7860 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -341,10 +341,10 @@ class Specifier(_IndividualSpecifier): # You cannot use a wild card and a dev or local version # together so group them with a | and make them optional. (?: + \.\* # Wild card syntax of .* + | (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local - | - \.\* # Wild card syntax of .* )? ) | From ed45c8c19eea7a6bc5a2eff8e95eb6be9d152a73 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Fri, 8 Jul 2022 14:50:55 +0100 Subject: [PATCH 2/7] Stop accepting legacy specifiers in `Requirement` --- packaging/requirements.py | 7 ++----- tests/test_requirements.py | 10 ++++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packaging/requirements.py b/packaging/requirements.py index bc0b17ca..79a044fd 100644 --- a/packaging/requirements.py +++ b/packaging/requirements.py @@ -21,7 +21,7 @@ ) from .markers import MARKER_EXPR as _MARKER_EXPR, Marker -from .specifiers import LegacySpecifier, Specifier, SpecifierSet +from .specifiers import Specifier, SpecifierSet class InvalidRequirement(ValueError): @@ -53,10 +53,7 @@ class InvalidRequirement(ValueError): EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA) EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras") -VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE) -VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE) - -VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY +VERSION_ONE = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE) VERSION_MANY = Combine( VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False )("_raw_spec") diff --git a/tests/test_requirements.py b/tests/test_requirements.py index ad07de76..5e4059df 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -60,14 +60,12 @@ def test_name_with_version(self): self._assert_requirement(req, "name", specifier=">=3") def test_with_legacy_version(self): - req = Requirement("name==1.0.org1") - self._assert_requirement(req, "name", specifier="==1.0.org1") + with pytest.raises(InvalidRequirement): + Requirement("name==1.0.org1") def test_with_legacy_version_and_marker(self): - req = Requirement("name>=1.x.y;python_version=='2.6'") - self._assert_requirement( - req, "name", specifier=">=1.x.y", marker='python_version == "2.6"' - ) + with pytest.raises(InvalidRequirement): + Requirement("name>=1.x.y;python_version=='2.6'") def test_version_with_parens_and_whitespace(self): req = Requirement("name (==4)") From 2151dc0f004beed90d191d9e1c8c5538a5ac3628 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Fri, 8 Jul 2022 14:38:52 +0100 Subject: [PATCH 3/7] Remove `LegacySpecifier` class This class has been deprecated for multiple releases now. --- docs/specifiers.rst | 54 +------------ packaging/specifiers.py | 137 ++++++++------------------------ tests/test_specifiers.py | 165 ++------------------------------------- 3 files changed, 44 insertions(+), 312 deletions(-) diff --git a/docs/specifiers.rst b/docs/specifiers.rst index 83299a8a..883f4b01 100644 --- a/docs/specifiers.rst +++ b/docs/specifiers.rst @@ -54,10 +54,8 @@ Reference can be passed a single specifier (``>=3.0``), a comma-separated list of specifiers (``>=3.0,!=3.1``), or no specifier at all. Each individual specifier will be attempted to be parsed as a PEP 440 specifier - (:class:`Specifier`) or as a legacy, setuptools style specifier - (deprecated :class:`LegacySpecifier`). You may combine - :class:`SpecifierSet` instances using the ``&`` operator - (``SpecifierSet(">2") & SpecifierSet("<4")``). + (:class:`Specifier`). You may combine :class:`SpecifierSet` instances using + the ``&`` operator (``SpecifierSet(">2") & SpecifierSet("<4")``). Both the membership tests and the combination support using raw strings in place of already instantiated objects. @@ -106,8 +104,8 @@ Reference .. method:: __iter__() - Returns an iterator over all the underlying :class:`Specifier` (or - deprecated :class:`LegacySpecifier`) instances in this specifier set. + Returns an iterator over all the underlying :class:`Specifier` instances + in this specifier set. .. method:: filter(iterable, prereleases=None) @@ -169,50 +167,6 @@ Reference See :meth:`SpecifierSet.filter()`. -.. class:: LegacySpecifier(specifier, prereleases=None) - - .. deprecated:: 20.5 - - Use :class:`Specifier` instead. - - This class abstracts the handling of a single legacy, setuptools style - specifier. It is generally not required to instantiate this manually, - preferring instead to work with :class:`SpecifierSet`. - - :param str specifier: The string representation of a specifier which will - be parsed and normalized before use. - :param bool prereleases: This tells the specifier if it should accept - prerelease versions if applicable or not. The - default of ``None`` will autodetect it from the - given specifiers. - :raises InvalidSpecifier: If the ``specifier`` is not parseable then this - will be raised. - - .. attribute:: operator - - The string value of the operator part of this specifier. - - .. attribute:: version - - The string version of the version part of this specifier. - - .. attribute:: prereleases - - See :attr:`SpecifierSet.prereleases`. - - .. method:: __contains__(version) - - See :meth:`SpecifierSet.__contains__()`. - - .. method:: contains(version, prereleases=None) - - See :meth:`SpecifierSet.contains()`. - - .. method:: filter(iterable, prereleases=None) - - See :meth:`SpecifierSet.filter()`. - - .. exception:: InvalidSpecifier Raised when attempting to create a :class:`Specifier` with a specifier diff --git a/packaging/specifiers.py b/packaging/specifiers.py index ec0d7860..678f3eec 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -6,7 +6,6 @@ import functools import itertools import re -import warnings from typing import ( Callable, Dict, @@ -17,17 +16,14 @@ Pattern, Set, Tuple, - TypeVar, Union, ) from .utils import canonicalize_version -from .version import LegacyVersion, Version, parse +from .version import Version, parse -ParsedVersion = Union[Version, LegacyVersion] -UnparsedVersion = Union[Version, LegacyVersion, str] -VersionTypeVar = TypeVar("VersionTypeVar", bound=UnparsedVersion) -CallableOperator = Callable[[ParsedVersion, str], bool] +UnparsedVersion = Union[Version, str] +CallableOperator = Callable[[Version, str], bool] class InvalidSpecifier(ValueError): @@ -80,8 +76,8 @@ def contains(self, item: str, prereleases: Optional[bool] = None) -> bool: @abc.abstractmethod def filter( - self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None - ) -> Iterable[VersionTypeVar]: + self, iterable: Iterable[UnparsedVersion], prereleases: Optional[bool] = None + ) -> Iterable[UnparsedVersion]: """ Takes an iterable of items and filters them so that only items which are contained within this specifier are allowed in it. @@ -146,8 +142,8 @@ def _get_operator(self, op: str) -> CallableOperator: ) return operator_callable - def _coerce_version(self, version: UnparsedVersion) -> ParsedVersion: - if not isinstance(version, (LegacyVersion, Version)): + def _coerce_version(self, version: UnparsedVersion) -> Version: + if not isinstance(version, Version): version = parse(version) return version @@ -178,8 +174,8 @@ def contains( if prereleases is None: prereleases = self.prereleases - # Normalize item to a Version or LegacyVersion, this allows us to have - # a shortcut for ``"2.0" in Specifier(">=2") + # Normalize item to a Version, this allows us to have a shortcut for + # "2.0" in Specifier(">=2") normalized_item = self._coerce_version(item) # Determine if we should be supporting prereleases in this specifier @@ -194,8 +190,8 @@ def contains( return operator_callable(normalized_item, self.version) def filter( - self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None - ) -> Iterable[VersionTypeVar]: + self, iterable: Iterable[UnparsedVersion], prereleases: Optional[bool] = None + ) -> Iterable[UnparsedVersion]: yielded = False found_prereleases = [] @@ -229,71 +225,11 @@ def filter( yield version -class LegacySpecifier(_IndividualSpecifier): - - _regex_str = r""" - (?P(==|!=|<=|>=|<|>)) - \s* - (?P - [^,;\s)]* # Since this is a "legacy" specifier, and the version - # string can be just about anything, we match everything - # except for whitespace, a semi-colon for marker support, - # a closing paren since versions can be enclosed in - # them, and a comma since it's a version separator. - ) - """ - - _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) - - _operators = { - "==": "equal", - "!=": "not_equal", - "<=": "less_than_equal", - ">=": "greater_than_equal", - "<": "less_than", - ">": "greater_than", - } - - def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: - super().__init__(spec, prereleases) - - warnings.warn( - "Creating a LegacyVersion has been deprecated and will be " - "removed in the next major release", - DeprecationWarning, - ) - - def _coerce_version(self, version: UnparsedVersion) -> LegacyVersion: - if not isinstance(version, LegacyVersion): - version = LegacyVersion(str(version)) - return version - - def _compare_equal(self, prospective: LegacyVersion, spec: str) -> bool: - return prospective == self._coerce_version(spec) - - def _compare_not_equal(self, prospective: LegacyVersion, spec: str) -> bool: - return prospective != self._coerce_version(spec) - - def _compare_less_than_equal(self, prospective: LegacyVersion, spec: str) -> bool: - return prospective <= self._coerce_version(spec) - - def _compare_greater_than_equal( - self, prospective: LegacyVersion, spec: str - ) -> bool: - return prospective >= self._coerce_version(spec) - - def _compare_less_than(self, prospective: LegacyVersion, spec: str) -> bool: - return prospective < self._coerce_version(spec) - - def _compare_greater_than(self, prospective: LegacyVersion, spec: str) -> bool: - return prospective > self._coerce_version(spec) - - def _require_version_compare( - fn: Callable[["Specifier", ParsedVersion, str], bool] -) -> Callable[["Specifier", ParsedVersion, str], bool]: + fn: Callable[["Specifier", Version, str], bool] +) -> Callable[["Specifier", Version, str], bool]: @functools.wraps(fn) - def wrapped(self: "Specifier", prospective: ParsedVersion, spec: str) -> bool: + def wrapped(self: "Specifier", prospective: Version, spec: str) -> bool: if not isinstance(prospective, Version): return False return fn(self, prospective, spec) @@ -410,7 +346,7 @@ class Specifier(_IndividualSpecifier): } @_require_version_compare - def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool: + def _compare_compatible(self, prospective: Version, spec: str) -> bool: # Compatible releases have an equivalent combination of >= and ==. That # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to @@ -432,7 +368,7 @@ def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool: ) @_require_version_compare - def _compare_equal(self, prospective: ParsedVersion, spec: str) -> bool: + def _compare_equal(self, prospective: Version, spec: str) -> bool: # We need special logic to handle prefix matching if spec.endswith(".*"): @@ -472,11 +408,11 @@ def _compare_equal(self, prospective: ParsedVersion, spec: str) -> bool: return prospective == spec_version @_require_version_compare - def _compare_not_equal(self, prospective: ParsedVersion, spec: str) -> bool: + def _compare_not_equal(self, prospective: Version, spec: str) -> bool: return not self._compare_equal(prospective, spec) @_require_version_compare - def _compare_less_than_equal(self, prospective: ParsedVersion, spec: str) -> bool: + def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool: # NB: Local version identifiers are NOT permitted in the version # specifier, so local version labels can be universally removed from @@ -484,9 +420,7 @@ def _compare_less_than_equal(self, prospective: ParsedVersion, spec: str) -> boo return Version(prospective.public) <= Version(spec) @_require_version_compare - def _compare_greater_than_equal( - self, prospective: ParsedVersion, spec: str - ) -> bool: + def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: # NB: Local version identifiers are NOT permitted in the version # specifier, so local version labels can be universally removed from @@ -494,7 +428,7 @@ def _compare_greater_than_equal( return Version(prospective.public) >= Version(spec) @_require_version_compare - def _compare_less_than(self, prospective: ParsedVersion, spec_str: str) -> bool: + def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: # Convert our spec to a Version instance, since we'll want to work with # it as a version. @@ -520,7 +454,7 @@ def _compare_less_than(self, prospective: ParsedVersion, spec_str: str) -> bool: return True @_require_version_compare - def _compare_greater_than(self, prospective: ParsedVersion, spec_str: str) -> bool: + def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: # Convert our spec to a Version instance, since we'll want to work with # it as a version. @@ -632,13 +566,10 @@ def __init__( split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] # Parsed each individual specifier, attempting first to make it a - # Specifier and falling back to a LegacySpecifier. + # Specifier. parsed: Set[_IndividualSpecifier] = set() for specifier in split_specifiers: - try: - parsed.add(Specifier(specifier)) - except InvalidSpecifier: - parsed.add(LegacySpecifier(specifier)) + parsed.add(Specifier(specifier)) # Turn our parsed specifiers into a frozen set and save them for later. self._specs = frozenset(parsed) @@ -731,8 +662,8 @@ def contains( installed: Optional[bool] = None, ) -> bool: - # Ensure that our item is a Version or LegacyVersion instance. - if not isinstance(item, (LegacyVersion, Version)): + # Ensure that our item is a Version instance. + if not isinstance(item, Version): item = parse(item) # Determine if we're forcing a prerelease or not, if we're not forcing @@ -760,8 +691,8 @@ def contains( return all(s.contains(item, prereleases=prereleases) for s in self._specs) def filter( - self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None - ) -> Iterable[VersionTypeVar]: + self, iterable: Iterable[UnparsedVersion], prereleases: Optional[bool] = None + ) -> Iterable[UnparsedVersion]: # Determine if we're forcing a prerelease or not, if we're not forcing # one for this particular filter call, then we'll use whatever the @@ -778,25 +709,21 @@ def filter( return iterable # If we do not have any specifiers, then we need to have a rough filter # which will filter out any pre-releases, unless there are no final - # releases, and which will filter out LegacyVersion in general. + # releases. else: - filtered: List[VersionTypeVar] = [] - found_prereleases: List[VersionTypeVar] = [] + filtered: List[UnparsedVersion] = [] + found_prereleases: List[UnparsedVersion] = [] item: UnparsedVersion - parsed_version: Union[Version, LegacyVersion] + parsed_version: Version for item in iterable: # Ensure that we some kind of Version class for this item. - if not isinstance(item, (LegacyVersion, Version)): + if not isinstance(item, Version): parsed_version = parse(item) else: parsed_version = item - # Filter out any item which is parsed as a LegacyVersion - if isinstance(parsed_version, LegacyVersion): - continue - # Store any item which is a pre-release for later unless we've # already found a final version or we are accepting prereleases if parsed_version.is_prerelease and not prereleases: diff --git a/tests/test_specifiers.py b/tests/test_specifiers.py index 5949ebf6..92d04eb0 100644 --- a/tests/test_specifiers.py +++ b/tests/test_specifiers.py @@ -4,19 +4,13 @@ import itertools import operator -import warnings import pytest -from packaging.specifiers import ( - InvalidSpecifier, - LegacySpecifier, - Specifier, - SpecifierSet, -) -from packaging.version import LegacyVersion, Version, parse +from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet +from packaging.version import Version, parse -from .test_version import LEGACY_VERSIONS, VERSIONS +from .test_version import VERSIONS LEGACY_SPECIFIERS = [ "==2.1.0.3", @@ -37,7 +31,6 @@ ">=7.9a1", "<1.0.dev1", ">2.0.post1", - "===lolwat", ] @@ -489,12 +482,10 @@ def test_specifiers(self, version, spec, expected): @pytest.mark.parametrize( ("version", "spec", "expected"), [ + ("1.0.0", "===1.0", False), + ("1.0.dev0", "===1.0", False), # Test identity comparison by itself - ("lolwat", "===lolwat", True), - ("Lolwat", "===lolwat", True), ("1.0", "===1.0", True), - ("nope", "===lolwat", False), - ("1.0.0", "===1.0", False), ("1.0.dev0", "===1.0.dev0", True), ], ) @@ -567,10 +558,6 @@ def test_specifier_filter(self, specifier, prereleases, input, expected): assert list(spec.filter(input, **kwargs)) == expected - @pytest.mark.xfail - def test_specifier_explicit_legacy(self): - assert Specifier("==1.0").contains(LegacyVersion("1.0")) - @pytest.mark.parametrize( ("spec", "op"), [ @@ -583,6 +570,7 @@ def test_specifier_explicit_legacy(self): (">=7.9a1", ">="), ("<1.0.dev1", "<"), (">2.0.post1", ">"), + # === is an escape hatch in PEP 440 ("===lolwat", "==="), ], ) @@ -601,6 +589,7 @@ def test_specifier_operator_property(self, spec, op): (">=7.9a1", "7.9a1"), ("<1.0.dev1", "1.0.dev1"), (">2.0.post1", "2.0.post1"), + # === is an escape hatch in PEP 440 ("===lolwat", "lolwat"), ], ) @@ -637,141 +626,8 @@ def test_specifier_hash_for_compatible_operator(self): assert hash(Specifier("~=1.18.0")) != hash(Specifier("~=1.18")) -class TestLegacySpecifier: - def test_legacy_specifier_is_deprecated(self): - with warnings.catch_warnings(record=True) as w: - LegacySpecifier(">=some-legacy-version") - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - - @pytest.mark.parametrize( - ("version", "spec", "expected"), - [ - (v, s, True) - for v, s in [ - # Test the equality operation - ("2.0", "==2"), - ("2.0", "==2.0"), - ("2.0", "==2.0.0"), - # Test the in-equality operation - ("2.1", "!=2"), - ("2.1", "!=2.0"), - ("2.0.1", "!=2"), - ("2.0.1", "!=2.0"), - ("2.0.1", "!=2.0.0"), - # Test the greater than equal operation - ("2.0", ">=2"), - ("2.0", ">=2.0"), - ("2.0", ">=2.0.0"), - ("2.0.post1", ">=2"), - ("2.0.post1.dev1", ">=2"), - ("3", ">=2"), - # Test the less than equal operation - ("2.0", "<=2"), - ("2.0", "<=2.0"), - ("2.0", "<=2.0.0"), - ("2.0.dev1", "<=2"), - ("2.0a1", "<=2"), - ("2.0a1.dev1", "<=2"), - ("2.0b1", "<=2"), - ("2.0b1.post1", "<=2"), - ("2.0c1", "<=2"), - ("2.0c1.post1.dev1", "<=2"), - ("2.0rc1", "<=2"), - ("1", "<=2"), - # Test the greater than operation - ("3", ">2"), - ("2.1", ">2.0"), - # Test the less than operation - ("1", "<2"), - ("2.0", "<2.1"), - ] - ] - + [ - (v, s, False) - for v, s in [ - # Test the equality operation - ("2.1", "==2"), - ("2.1", "==2.0"), - ("2.1", "==2.0.0"), - # Test the in-equality operation - ("2.0", "!=2"), - ("2.0", "!=2.0"), - ("2.0", "!=2.0.0"), - # Test the greater than equal operation - ("2.0.dev1", ">=2"), - ("2.0a1", ">=2"), - ("2.0a1.dev1", ">=2"), - ("2.0b1", ">=2"), - ("2.0b1.post1", ">=2"), - ("2.0c1", ">=2"), - ("2.0c1.post1.dev1", ">=2"), - ("2.0rc1", ">=2"), - ("1", ">=2"), - # Test the less than equal operation - ("2.0.post1", "<=2"), - ("2.0.post1.dev1", "<=2"), - ("3", "<=2"), - # Test the greater than operation - ("1", ">2"), - ("2.0.dev1", ">2"), - ("2.0a1", ">2"), - ("2.0a1.post1", ">2"), - ("2.0b1", ">2"), - ("2.0b1.dev1", ">2"), - ("2.0c1", ">2"), - ("2.0c1.post1.dev1", ">2"), - ("2.0rc1", ">2"), - ("2.0", ">2"), - # Test the less than operation - ("3", "<2"), - ] - ], - ) - def test_specifiers(self, version, spec, expected): - spec = LegacySpecifier(spec, prereleases=True) - - if expected: - # Test that the plain string form works - assert version in spec - assert spec.contains(version) - - # Test that the version instance form works - assert LegacyVersion(version) in spec - assert spec.contains(LegacyVersion(version)) - else: - # Test that the plain string form works - assert version not in spec - assert not spec.contains(version) - - # Test that the version instance form works - assert LegacyVersion(version) not in spec - assert not spec.contains(LegacyVersion(version)) - - def test_specifier_explicit_prereleases(self): - spec = LegacySpecifier(">=1.0") - assert not spec.prereleases - spec.prereleases = True - assert spec.prereleases - - spec = LegacySpecifier(">=1.0", prereleases=False) - assert not spec.prereleases - spec.prereleases = True - assert spec.prereleases - - spec = LegacySpecifier(">=1.0", prereleases=True) - assert spec.prereleases - spec.prereleases = False - assert not spec.prereleases - - spec = LegacySpecifier(">=1.0", prereleases=True) - assert spec.prereleases - spec.prereleases = None - assert not spec.prereleases - - class TestSpecifierSet: - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) + @pytest.mark.parametrize("version", VERSIONS) def test_empty_specifier(self, version): spec = SpecifierSet(prereleases=True) @@ -836,7 +692,6 @@ def test_specifier_contains_installed_prereleases(self): (">=1.0.dev1", None, None, ["1.0", "2.0a1"], ["1.0", "2.0a1"]), ("", None, None, ["1.0a1"], ["1.0a1"]), ("", None, None, ["1.0", Version("2.0")], ["1.0", Version("2.0")]), - ("", None, None, ["2.0dog", "1.0"], ["1.0"]), # Test overriding with the prereleases parameter on filter ("", None, False, ["1.0a1"], []), (">=1.0.dev1", None, False, ["1.0", "2.0a1"], ["1.0"]), @@ -862,10 +717,6 @@ def test_specifier_filter( assert list(spec.filter(input, **kwargs)) == expected - def test_legacy_specifiers_combined(self): - spec = SpecifierSet("<3,>1-1-1") - assert "2.0" in spec - @pytest.mark.parametrize( ("specifier", "expected"), [ From 2a16754afa1f69f150d1978faff2fc53e52cada6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Fri, 8 Jul 2022 14:39:09 +0100 Subject: [PATCH 4/7] Remove `LegacyVersion` class This class has been deprecated for multiple releases now. --- docs/specifiers.rst | 8 +-- docs/version.rst | 138 +-------------------------------------- packaging/version.py | 144 ++++------------------------------------- tests/test_utils.py | 4 +- tests/test_version.py | 147 +++--------------------------------------- 5 files changed, 27 insertions(+), 414 deletions(-) diff --git a/docs/specifiers.rst b/docs/specifiers.rst index 883f4b01..253c5107 100644 --- a/docs/specifiers.rst +++ b/docs/specifiers.rst @@ -89,8 +89,7 @@ Reference .. method:: contains(version, prereleases=None) Determines if ``version``, which can be either a version string, a - :class:`Version`, or a deprecated :class:`LegacyVersion` object, is - contained within this set of specifiers. + :class:`Version` is contained within this set of specifiers. This will either match or not match prereleases based on the ``prereleases`` parameter. When ``prereleases`` is set to ``None`` @@ -110,9 +109,8 @@ Reference .. method:: filter(iterable, prereleases=None) Takes an iterable that can contain version strings, :class:`~.Version`, - and deprecated :class:`~.LegacyVersion` instances and will then filter - it, returning an iterable that contains only items which match the - rules of this specifier object. + instances and will then filter them, returning an iterable that contains + only items which match the rules of this specifier object. This method is smarter than just ``filter(Specifier().contains, [...])`` because it implements the rule diff --git a/docs/version.rst b/docs/version.rst index a43cf786..73a2a01a 100644 --- a/docs/version.rst +++ b/docs/version.rst @@ -50,8 +50,8 @@ Reference .. function:: parse(version) This function takes a version string and will parse it as a - :class:`Version` if the version is a valid PEP 440 version, otherwise it - will parse it as a deprecated :class:`LegacyVersion`. + :class:`Version` if the version is a valid PEP 440 version. + Otherwise, raises :class:`InvalidVersion`. .. class:: Version(version) @@ -138,140 +138,6 @@ Reference represents a post-release. -.. class:: LegacyVersion(version) - - .. deprecated:: 20.5 - - Use :class:`Version` instead. - - This class abstracts handling of a project's versions if they are not - compatible with the scheme defined in `PEP 440`_. It implements a similar - interface to that of :class:`Version`. - - This class implements the previous de facto sorting algorithm used by - setuptools, however it will always sort as less than a :class:`Version` - instance. - - :param str version: The string representation of a version which will be - used as is. - - .. note:: - - :class:`LegacyVersion` instances are always ordered lower than :class:`Version` instances. - - >>> from packaging.version import Version, LegacyVersion - >>> v1 = Version("1.0") - >>> v2 = LegacyVersion("1.0") - >>> v1 > v2 - True - >>> v3 = LegacyVersion("1.3") - >>> v1 > v3 - True - - Also note that some strings are still valid PEP 440 strings (:class:`Version`), even if they look very similar to - other versions that are not (:class:`LegacyVersion`). Examples include versions with `Pre-release spelling`_ and - `Post-release spelling`_. - - >>> from packaging.version import parse - >>> v1 = parse('0.9.8a') - >>> v2 = parse('0.9.8beta') - >>> v3 = parse('0.9.8r') - >>> v4 = parse('0.9.8rev') - >>> v5 = parse('0.9.8t') - >>> v1 - - >>> v1.is_prerelease - True - >>> v2 - - >>> v2.is_prerelease - True - >>> v3 - - >>> v3.is_postrelease - True - >>> v4 - - >>> v4.is_postrelease - True - >>> v5 - - >>> v5.is_prerelease - False - >>> v5.is_postrelease - False - - .. attribute:: public - - A string representing the public version portion of this - :class:`LegacyVersion`. This will always be the entire version string. - - .. attribute:: base_version - - A string representing the base version portion of this - :class:`LegacyVersion` instance. This will always be the entire version - string. - - .. attribute:: epoch - - This will always be ``-1`` since without `PEP 440`_ we do not have the - concept of version epochs. The value reflects the fact that - :class:`LegacyVersion` instances always compare less than - :class:`Version` instances. - - .. attribute:: release - - This will always be ``None`` since without `PEP 440`_ we do not have - the concept of a release segment or its components. It exists - primarily to allow a :class:`LegacyVersion` to be used as a stand in - for a :class:`Version`. - - .. attribute:: local - - This will always be ``None`` since without `PEP 440`_ we do not have - the concept of a local version. It exists primarily to allow a - :class:`LegacyVersion` to be used as a stand in for a :class:`Version`. - - .. attribute:: pre - - This will always be ``None`` since without `PEP 440`_ we do not have - the concept of a prerelease. It exists primarily to allow a - :class:`LegacyVersion` to be used as a stand in for a :class:`Version`. - - .. attribute:: is_prerelease - - A boolean value indicating whether this :class:`LegacyVersion` - represents a prerelease and/or development release. Since without - `PEP 440`_ there is no concept of pre or dev releases this will - always be `False` and exists for compatibility with :class:`Version`. - - .. attribute:: dev - - This will always be ``None`` since without `PEP 440`_ we do not have - the concept of a development release. It exists primarily to allow a - :class:`LegacyVersion` to be used as a stand in for a :class:`Version`. - - .. attribute:: is_devrelease - - A boolean value indicating whether this :class:`LegacyVersion` - represents a development release. Since without `PEP 440`_ there is - no concept of dev releases this will always be `False` and exists for - compatibility with :class:`Version`. - - .. attribute:: post - - This will always be ``None`` since without `PEP 440`_ we do not have - the concept of a postrelease. It exists primarily to allow a - :class:`LegacyVersion` to be used as a stand in for a :class:`Version`. - - .. attribute:: is_postrelease - - A boolean value indicating whether this :class:`LegacyVersion` - represents a post-release. Since without `PEP 440`_ there is no concept - of post-releases this will always be ``False`` and exists for - compatibility with :class:`Version`. - - .. exception:: InvalidVersion Raised when attempting to create a :class:`Version` with a version string diff --git a/packaging/version.py b/packaging/version.py index de9a09a4..9a23b8e2 100644 --- a/packaging/version.py +++ b/packaging/version.py @@ -5,12 +5,11 @@ import collections import itertools import re -import warnings -from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union +from typing import Callable, Optional, SupportsInt, Tuple, Union from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType -__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] +__all__ = ["parse", "Version", "InvalidVersion", "VERSION_PATTERN"] InfiniteTypes = Union[InfinityType, NegativeInfinityType] PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] @@ -29,26 +28,22 @@ CmpKey = Tuple[ int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType ] -LegacyCmpKey = Tuple[int, Tuple[str, ...]] -VersionComparisonMethod = Callable[ - [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool -] +VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] _Version = collections.namedtuple( "_Version", ["epoch", "release", "dev", "pre", "post", "local"] ) -def parse(version: str) -> Union["LegacyVersion", "Version"]: +def parse(version: str) -> "Version": """ - Parse the given version string and return either a :class:`Version` object - or a :class:`LegacyVersion` object depending on if the given version is - a valid PEP 440 version or a legacy version. + Parse the given version string. + + Returns a :class:`Version` object, if the given version is a valid PEP 440 version. + + Raises :class:`InvalidVersion` otherwise. """ - try: - return Version(version) - except InvalidVersion: - return LegacyVersion(version) + return Version(version) class InvalidVersion(ValueError): @@ -58,7 +53,7 @@ class InvalidVersion(ValueError): class _BaseVersion: - _key: Union[CmpKey, LegacyCmpKey] + _key: CmpKey def __hash__(self) -> int: return hash(self._key) @@ -103,123 +98,6 @@ def __ne__(self, other: object) -> bool: return self._key != other._key -class LegacyVersion(_BaseVersion): - def __init__(self, version: str) -> None: - self._version = str(version) - self._key = _legacy_cmpkey(self._version) - - warnings.warn( - "Creating a LegacyVersion has been deprecated and will be " - "removed in the next major release", - DeprecationWarning, - ) - - def __str__(self) -> str: - return self._version - - def __repr__(self) -> str: - return f"" - - @property - def public(self) -> str: - return self._version - - @property - def base_version(self) -> str: - return self._version - - @property - def epoch(self) -> int: - return -1 - - @property - def release(self) -> None: - return None - - @property - def pre(self) -> None: - return None - - @property - def post(self) -> None: - return None - - @property - def dev(self) -> None: - return None - - @property - def local(self) -> None: - return None - - @property - def is_prerelease(self) -> bool: - return False - - @property - def is_postrelease(self) -> bool: - return False - - @property - def is_devrelease(self) -> bool: - return False - - -_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) - -_legacy_version_replacement_map = { - "pre": "c", - "preview": "c", - "-": "final-", - "rc": "c", - "dev": "@", -} - - -def _parse_version_parts(s: str) -> Iterator[str]: - for part in _legacy_version_component_re.split(s): - part = _legacy_version_replacement_map.get(part, part) - - if not part or part == ".": - continue - - if part[:1] in "0123456789": - # pad for numeric comparison - yield part.zfill(8) - else: - yield "*" + part - - # ensure that alpha/beta/candidate are before final - yield "*final" - - -def _legacy_cmpkey(version: str) -> LegacyCmpKey: - - # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch - # greater than or equal to 0. This will effectively put the LegacyVersion, - # which uses the defacto standard originally implemented by setuptools, - # as before all PEP 440 versions. - epoch = -1 - - # This scheme is taken from pkg_resources.parse_version setuptools prior to - # it's adoption of the packaging library. - parts: List[str] = [] - for part in _parse_version_parts(version.lower()): - if part.startswith("*"): - # remove "-" before a prerelease tag - if part < "*final": - while parts and parts[-1] == "*final-": - parts.pop() - - # remove trailing zeros from each series of numeric parts - while parts and parts[-1] == "00000000": - parts.pop() - - parts.append(part) - - return epoch, tuple(parts) - - # Deliberately not anchored to the start and end of the string, to make it # easier for 3rd party code to reuse VERSION_PATTERN = r""" diff --git a/tests/test_utils.py b/tests/test_utils.py index 84a8b38b..a6c6711d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -49,7 +49,9 @@ def test_canonicalize_name(name, expected): ("1.0a0", "1a0"), ("1.0rc0", "1rc0"), ("100!0.0", "100!0"), - ("1.0.1-test7", "1.0.1-test7"), # LegacyVersion is unchanged + # improper version strings are unchanged + ("lolwat", "lolwat"), + ("1.0.1-test7", "1.0.1-test7"), ], ) def test_canonicalize_version(version, expected): diff --git a/tests/test_version.py b/tests/test_version.py index 5f2251e1..8004c0cc 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -4,19 +4,20 @@ import itertools import operator -import warnings import pretend import pytest -from packaging.version import InvalidVersion, LegacyVersion, Version, parse +from packaging.version import InvalidVersion, Version, parse -@pytest.mark.parametrize( - ("version", "klass"), [("1.0", Version), ("1-1-1", LegacyVersion)] -) -def test_parse(version, klass): - assert isinstance(parse(version), klass) +def test_parse(): + assert isinstance(parse("1.0"), Version) + + +def test_parse_raises(): + with pytest.raises(InvalidVersion): + parse("lolwat") # This list must be in the correct sorting order @@ -759,10 +760,6 @@ def test_compare_other(self, op, expected): assert getattr(operator, op)(Version("1"), other) is expected - def test_compare_legacyversion_version(self): - result = sorted([Version("0"), LegacyVersion("1")]) - assert result == [LegacyVersion("1"), Version("0")] - def test_major_version(self): assert Version("2.1.0").major == 2 @@ -774,131 +771,3 @@ def test_micro_version(self): assert Version("2.1.3").micro == 3 assert Version("2.1").micro == 0 assert Version("2").micro == 0 - - -LEGACY_VERSIONS = ["foobar", "a cat is fine too", "lolwut", "1-0", "2.0-a1"] - - -class TestLegacyVersion: - def test_legacy_version_is_deprecated(self): - with warnings.catch_warnings(record=True) as w: - LegacyVersion("some-legacy-version") - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_valid_legacy_versions(self, version): - LegacyVersion(version) - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_str_repr(self, version): - assert str(LegacyVersion(version)) == version - assert repr(LegacyVersion(version)) == "".format( - repr(version) - ) - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_hash(self, version): - assert hash(LegacyVersion(version)) == hash(LegacyVersion(version)) - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_public(self, version): - assert LegacyVersion(version).public == version - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_base_version(self, version): - assert LegacyVersion(version).base_version == version - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_epoch(self, version): - assert LegacyVersion(version).epoch == -1 - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_release(self, version): - assert LegacyVersion(version).release is None - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_local(self, version): - assert LegacyVersion(version).local is None - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_pre(self, version): - assert LegacyVersion(version).pre is None - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_is_prerelease(self, version): - assert not LegacyVersion(version).is_prerelease - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_dev(self, version): - assert LegacyVersion(version).dev is None - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_is_devrelease(self, version): - assert not LegacyVersion(version).is_devrelease - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_post(self, version): - assert LegacyVersion(version).post is None - - @pytest.mark.parametrize("version", VERSIONS + LEGACY_VERSIONS) - def test_legacy_version_is_postrelease(self, version): - assert not LegacyVersion(version).is_postrelease - - @pytest.mark.parametrize( - ("left", "right", "op"), - # Below we'll generate every possible combination of - # VERSIONS + LEGACY_VERSIONS that should be True for the given operator - itertools.chain( - * - # Verify that the equal (==) operator works correctly - [[(x, x, operator.eq) for x in VERSIONS + LEGACY_VERSIONS]] - + - # Verify that the not equal (!=) operator works correctly - [ - [ - (x, y, operator.ne) - for j, y in enumerate(VERSIONS + LEGACY_VERSIONS) - if i != j - ] - for i, x in enumerate(VERSIONS + LEGACY_VERSIONS) - ] - ), - ) - def test_comparison_true(self, left, right, op): - assert op(LegacyVersion(left), LegacyVersion(right)) - - @pytest.mark.parametrize( - ("left", "right", "op"), - # Below we'll generate every possible combination of - # VERSIONS + LEGACY_VERSIONS that should be False for the given - # operator - itertools.chain( - * - # Verify that the equal (==) operator works correctly - [ - [ - (x, y, operator.eq) - for j, y in enumerate(VERSIONS + LEGACY_VERSIONS) - if i != j - ] - for i, x in enumerate(VERSIONS + LEGACY_VERSIONS) - ] - + - # Verify that the not equal (!=) operator works correctly - [[(x, x, operator.ne) for x in VERSIONS + LEGACY_VERSIONS]] - ), - ) - def test_comparison_false(self, left, right, op): - assert not op(LegacyVersion(left), LegacyVersion(right)) - - @pytest.mark.parametrize("op", ["lt", "le", "eq", "ge", "gt", "ne"]) - def test_dunder_op_returns_notimplemented(self, op): - method = getattr(LegacyVersion, f"__{op}__") - assert method(LegacyVersion("1"), 1) is NotImplemented - - @pytest.mark.parametrize(("op", "expected"), [("eq", False), ("ne", True)]) - def test_compare_other(self, op, expected): - other = pretend.stub(**{f"__{op}__": lambda other: NotImplemented}) - - assert getattr(operator, op)(LegacyVersion("1"), other) is expected From 134b659bcdc956c64c9d341b8a54c9dcc1156d1d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 28 Feb 2021 09:56:14 +0000 Subject: [PATCH 5/7] Merge `_IndividualSpecifier` and `Specifier` This also eliminates the `_IndividualSpecifier.prereleases`, which is no longer used. --- packaging/specifiers.py | 289 ++++++++++++++++++---------------------- 1 file changed, 132 insertions(+), 157 deletions(-) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index 678f3eec..a28526b6 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -6,18 +6,7 @@ import functools import itertools import re -from typing import ( - Callable, - Dict, - Iterable, - Iterator, - List, - Optional, - Pattern, - Set, - Tuple, - Union, -) +from typing import Callable, Iterable, Iterator, List, Optional, Set, Tuple, Union from .utils import canonicalize_version from .version import Version, parse @@ -84,147 +73,6 @@ def filter( """ -class _IndividualSpecifier(BaseSpecifier): - - _operators: Dict[str, str] = {} - _regex: Pattern[str] - - def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: - match = self._regex.search(spec) - if not match: - raise InvalidSpecifier(f"Invalid specifier: '{spec}'") - - self._spec: Tuple[str, str] = ( - match.group("operator").strip(), - match.group("version").strip(), - ) - - # Store whether or not this Specifier should accept prereleases - self._prereleases = prereleases - - def __repr__(self) -> str: - pre = ( - f", prereleases={self.prereleases!r}" - if self._prereleases is not None - else "" - ) - - return f"<{self.__class__.__name__}({str(self)!r}{pre})>" - - def __str__(self) -> str: - return "{}{}".format(*self._spec) - - @property - def _canonical_spec(self) -> Tuple[str, str]: - canonical_version = canonicalize_version( - self._spec[1], - strip_trailing_zero=(self._spec[0] != "~="), - ) - return self._spec[0], canonical_version - - def __hash__(self) -> int: - return hash(self._canonical_spec) - - def __eq__(self, other: object) -> bool: - if isinstance(other, str): - try: - other = self.__class__(str(other)) - except InvalidSpecifier: - return NotImplemented - elif not isinstance(other, self.__class__): - return NotImplemented - - return self._canonical_spec == other._canonical_spec - - def _get_operator(self, op: str) -> CallableOperator: - operator_callable: CallableOperator = getattr( - self, f"_compare_{self._operators[op]}" - ) - return operator_callable - - def _coerce_version(self, version: UnparsedVersion) -> Version: - if not isinstance(version, Version): - version = parse(version) - return version - - @property - def operator(self) -> str: - return self._spec[0] - - @property - def version(self) -> str: - return self._spec[1] - - @property - def prereleases(self) -> Optional[bool]: - return self._prereleases - - @prereleases.setter - def prereleases(self, value: bool) -> None: - self._prereleases = value - - def __contains__(self, item: str) -> bool: - return self.contains(item) - - def contains( - self, item: UnparsedVersion, prereleases: Optional[bool] = None - ) -> bool: - - # Determine if prereleases are to be allowed or not. - if prereleases is None: - prereleases = self.prereleases - - # Normalize item to a Version, this allows us to have a shortcut for - # "2.0" in Specifier(">=2") - normalized_item = self._coerce_version(item) - - # Determine if we should be supporting prereleases in this specifier - # or not, if we do not support prereleases than we can short circuit - # logic if this version is a prereleases. - if normalized_item.is_prerelease and not prereleases: - return False - - # Actually do the comparison to determine if this item is contained - # within this Specifier or not. - operator_callable: CallableOperator = self._get_operator(self.operator) - return operator_callable(normalized_item, self.version) - - def filter( - self, iterable: Iterable[UnparsedVersion], prereleases: Optional[bool] = None - ) -> Iterable[UnparsedVersion]: - - yielded = False - found_prereleases = [] - - kw = {"prereleases": prereleases if prereleases is not None else True} - - # Attempt to iterate over all the values in the iterable and if any of - # them match, yield them. - for version in iterable: - parsed_version = self._coerce_version(version) - - if self.contains(parsed_version, **kw): - # If our version is a prerelease, and we were not set to allow - # prereleases, then we'll store it for later in case nothing - # else matches this specifier. - if parsed_version.is_prerelease and not ( - prereleases or self.prereleases - ): - found_prereleases.append(version) - # Either this is not a prerelease, or we should have been - # accepting prereleases from the beginning. - else: - yielded = True - yield version - - # Now that we've iterated over everything, determine if we've yielded - # any values, and if we have not and we have any prereleases stored up - # then we will go ahead and yield the prereleases. - if not yielded and found_prereleases: - for version in found_prereleases: - yield version - - def _require_version_compare( fn: Callable[["Specifier", Version, str], bool] ) -> Callable[["Specifier", Version, str], bool]: @@ -237,7 +85,7 @@ def wrapped(self: "Specifier", prospective: Version, spec: str) -> bool: return wrapped -class Specifier(_IndividualSpecifier): +class Specifier(BaseSpecifier): _regex_str = r""" (?P(~=|==|!=|<=|>=|<|>|===)) @@ -345,6 +193,64 @@ class Specifier(_IndividualSpecifier): "===": "arbitrary", } + def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: + match = self._regex.search(spec) + if not match: + raise InvalidSpecifier(f"Invalid specifier: '{spec}'") + + self._spec: Tuple[str, str] = ( + match.group("operator").strip(), + match.group("version").strip(), + ) + + # Store whether or not this Specifier should accept prereleases + self._prereleases = prereleases + + def __repr__(self) -> str: + pre = ( + f", prereleases={self.prereleases!r}" + if self._prereleases is not None + else "" + ) + + return f"<{self.__class__.__name__}({str(self)!r}{pre})>" + + def __str__(self) -> str: + return "{}{}".format(*self._spec) + + @property + def _canonical_spec(self) -> Tuple[str, str]: + canonical_version = canonicalize_version( + self._spec[1], + strip_trailing_zero=(self._spec[0] != "~="), + ) + return self._spec[0], canonical_version + + def __hash__(self) -> int: + return hash(self._canonical_spec) + + def __eq__(self, other: object) -> bool: + if isinstance(other, str): + try: + other = self.__class__(str(other)) + except InvalidSpecifier: + return NotImplemented + elif not isinstance(other, self.__class__): + return NotImplemented + + return self._canonical_spec == other._canonical_spec + + def _get_operator(self, op: str) -> CallableOperator: + operator_callable: CallableOperator = getattr( + self, f"_compare_{self._operators[op]}" + ) + return operator_callable + + def _coerce_version(self, version: UnparsedVersion) -> Version: + if not isinstance(version, Version): + version = parse(version) + return version + @_require_version_compare def _compare_compatible(self, prospective: Version, spec: str) -> bool: @@ -488,6 +394,75 @@ def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: return str(prospective).lower() == str(spec).lower() + @property + def operator(self) -> str: + return self._spec[0] + + @property + def version(self) -> str: + return self._spec[1] + + def __contains__(self, item: str) -> bool: + return self.contains(item) + + def contains( + self, item: UnparsedVersion, prereleases: Optional[bool] = None + ) -> bool: + + # Determine if prereleases are to be allowed or not. + if prereleases is None: + prereleases = self.prereleases + + # Normalize item to a Version, this allows us to have a shortcut for + # "2.0" in Specifier(">=2") + normalized_item = self._coerce_version(item) + + # Determine if we should be supporting prereleases in this specifier + # or not, if we do not support prereleases than we can short circuit + # logic if this version is a prereleases. + if normalized_item.is_prerelease and not prereleases: + return False + + # Actually do the comparison to determine if this item is contained + # within this Specifier or not. + operator_callable: CallableOperator = self._get_operator(self.operator) + return operator_callable(normalized_item, self.version) + + def filter( + self, iterable: Iterable[UnparsedVersion], prereleases: Optional[bool] = None + ) -> Iterable[UnparsedVersion]: + + yielded = False + found_prereleases = [] + + kw = {"prereleases": prereleases if prereleases is not None else True} + + # Attempt to iterate over all the values in the iterable and if any of + # them match, yield them. + for version in iterable: + parsed_version = self._coerce_version(version) + + if self.contains(parsed_version, **kw): + # If our version is a prerelease, and we were not set to allow + # prereleases, then we'll store it for later incase nothing + # else matches this specifier. + if parsed_version.is_prerelease and not ( + prereleases or self.prereleases + ): + found_prereleases.append(version) + # Either this is not a prerelease, or we should have been + # accepting prereleases from the beginning. + else: + yielded = True + yield version + + # Now that we've iterated over everything, determine if we've yielded + # any values, and if we have not and we have any prereleases stored up + # then we will go ahead and yield the prereleases. + if not yielded and found_prereleases: + for version in found_prereleases: + yield version + @property def prereleases(self) -> bool: @@ -567,7 +542,7 @@ def __init__( # Parsed each individual specifier, attempting first to make it a # Specifier. - parsed: Set[_IndividualSpecifier] = set() + parsed: Set[Specifier] = set() for specifier in split_specifiers: parsed.add(Specifier(specifier)) @@ -617,7 +592,7 @@ def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet": return specifier def __eq__(self, other: object) -> bool: - if isinstance(other, (str, _IndividualSpecifier)): + if isinstance(other, (str, Specifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): return NotImplemented @@ -627,7 +602,7 @@ def __eq__(self, other: object) -> bool: def __len__(self) -> int: return len(self._specs) - def __iter__(self) -> Iterator[_IndividualSpecifier]: + def __iter__(self) -> Iterator[Specifier]: return iter(self._specs) @property From c63e26c62e7c499acac5d07a2eca4e82dc80c10c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 28 Feb 2021 10:00:21 +0000 Subject: [PATCH 6/7] Drop `_require_version_compare` This enforced that a `LegacyVersion` could not match a `Specifier`. Since `LegacyVersion` is no longer a thing, this check is no longer necessary. --- packaging/specifiers.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index a28526b6..404b7686 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -73,18 +73,6 @@ def filter( """ -def _require_version_compare( - fn: Callable[["Specifier", Version, str], bool] -) -> Callable[["Specifier", Version, str], bool]: - @functools.wraps(fn) - def wrapped(self: "Specifier", prospective: Version, spec: str) -> bool: - if not isinstance(prospective, Version): - return False - return fn(self, prospective, spec) - - return wrapped - - class Specifier(BaseSpecifier): _regex_str = r""" @@ -251,7 +239,6 @@ def _coerce_version(self, version: UnparsedVersion) -> Version: version = parse(version) return version - @_require_version_compare def _compare_compatible(self, prospective: Version, spec: str) -> bool: # Compatible releases have an equivalent combination of >= and ==. That @@ -273,7 +260,6 @@ def _compare_compatible(self, prospective: Version, spec: str) -> bool: prospective, prefix ) - @_require_version_compare def _compare_equal(self, prospective: Version, spec: str) -> bool: # We need special logic to handle prefix matching @@ -313,11 +299,9 @@ def _compare_equal(self, prospective: Version, spec: str) -> bool: return prospective == spec_version - @_require_version_compare def _compare_not_equal(self, prospective: Version, spec: str) -> bool: return not self._compare_equal(prospective, spec) - @_require_version_compare def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool: # NB: Local version identifiers are NOT permitted in the version @@ -325,7 +309,6 @@ def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool: # the prospective version. return Version(prospective.public) <= Version(spec) - @_require_version_compare def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: # NB: Local version identifiers are NOT permitted in the version @@ -333,7 +316,6 @@ def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: # the prospective version. return Version(prospective.public) >= Version(spec) - @_require_version_compare def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: # Convert our spec to a Version instance, since we'll want to work with @@ -359,7 +341,6 @@ def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: # version in the spec. return True - @_require_version_compare def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: # Convert our spec to a Version instance, since we'll want to work with From 5943a3800db12302f3be328a1bac9ad32f4f26eb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Fri, 8 Jul 2022 11:35:36 +0100 Subject: [PATCH 7/7] Factor out `_coerce_version` and use it to simplify a loop This makes the loop easier to understand. --- packaging/specifiers.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index 404b7686..dab49eef 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -3,18 +3,23 @@ # for complete details. import abc -import functools import itertools import re from typing import Callable, Iterable, Iterator, List, Optional, Set, Tuple, Union from .utils import canonicalize_version -from .version import Version, parse +from .version import Version UnparsedVersion = Union[Version, str] CallableOperator = Callable[[Version, str], bool] +def _coerce_version(version: UnparsedVersion) -> Version: + if not isinstance(version, Version): + version = Version(version) + return version + + class InvalidSpecifier(ValueError): """ An invalid specifier was found, users should refer to PEP 440. @@ -234,11 +239,6 @@ def _get_operator(self, op: str) -> CallableOperator: ) return operator_callable - def _coerce_version(self, version: UnparsedVersion) -> Version: - if not isinstance(version, Version): - version = parse(version) - return version - def _compare_compatible(self, prospective: Version, spec: str) -> bool: # Compatible releases have an equivalent combination of >= and ==. That @@ -396,7 +396,7 @@ def contains( # Normalize item to a Version, this allows us to have a shortcut for # "2.0" in Specifier(">=2") - normalized_item = self._coerce_version(item) + normalized_item = _coerce_version(item) # Determine if we should be supporting prereleases in this specifier # or not, if we do not support prereleases than we can short circuit @@ -421,7 +421,7 @@ def filter( # Attempt to iterate over all the values in the iterable and if any of # them match, yield them. for version in iterable: - parsed_version = self._coerce_version(version) + parsed_version = _coerce_version(version) if self.contains(parsed_version, **kw): # If our version is a prerelease, and we were not set to allow @@ -464,7 +464,7 @@ def prereleases(self) -> bool: # Parse the version, and if it is a pre-release than this # specifier allows pre-releases. - if parse(version).is_prerelease: + if Version(version).is_prerelease: return True return False @@ -620,7 +620,7 @@ def contains( # Ensure that our item is a Version instance. if not isinstance(item, Version): - item = parse(item) + item = Version(item) # Determine if we're forcing a prerelease or not, if we're not forcing # one for this particular filter call, then we'll use whatever the @@ -638,7 +638,7 @@ def contains( return False if installed and item.is_prerelease: - item = parse(item.base_version) + item = Version(item.base_version) # We simply dispatch to the underlying specs here to make sure that the # given version is contained within all of them. @@ -670,15 +670,8 @@ def filter( filtered: List[UnparsedVersion] = [] found_prereleases: List[UnparsedVersion] = [] - item: UnparsedVersion - parsed_version: Version - for item in iterable: - # Ensure that we some kind of Version class for this item. - if not isinstance(item, Version): - parsed_version = parse(item) - else: - parsed_version = item + parsed_version = _coerce_version(item) # Store any item which is a pre-release for later unless we've # already found a final version or we are accepting prereleases