From 237ff3aa348486cf835a980592af3a59fccd6101 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Fri, 8 Jul 2022 20:58:21 +0100 Subject: [PATCH] Drop `LegacySpecifier` and `LegacyVersion` (#407) * 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`. * Stop accepting legacy specifiers in `Requirement` * Remove `LegacySpecifier` class This class has been deprecated for multiple releases now. * Remove `LegacyVersion` class This class has been deprecated for multiple releases now. * Merge `_IndividualSpecifier` and `Specifier` This also eliminates the `_IndividualSpecifier.prereleases`, which is no longer used. * 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. * Factor out `_coerce_version` and use it to simplify a loop This makes the loop easier to understand. Co-authored-by: Pradyun Gedam --- docs/specifiers.rst | 62 +----- docs/version.rst | 138 +----------- packaging/requirements.py | 7 +- packaging/specifiers.py | 444 +++++++++++++------------------------ packaging/version.py | 144 +----------- tests/test_requirements.py | 10 +- tests/test_specifiers.py | 165 +------------- tests/test_utils.py | 4 +- tests/test_version.py | 147 +----------- 9 files changed, 205 insertions(+), 916 deletions(-) diff --git a/docs/specifiers.rst b/docs/specifiers.rst index 83299a8a..253c5107 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. @@ -91,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`` @@ -106,15 +103,14 @@ 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) 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 @@ -169,50 +165,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/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/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/packaging/specifiers.py b/packaging/specifiers.py index a2d51b04..dab49eef 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -3,31 +3,21 @@ # for complete details. import abc -import functools import itertools import re -import warnings -from typing import ( - Callable, - Dict, - Iterable, - Iterator, - List, - Optional, - Pattern, - Set, - Tuple, - TypeVar, - Union, -) +from typing import Callable, Iterable, Iterator, List, Optional, Set, Tuple, Union from .utils import canonicalize_version -from .version import LegacyVersion, Version, parse +from .version import Version -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] + + +def _coerce_version(version: UnparsedVersion) -> Version: + if not isinstance(version, Version): + version = Version(version) + return version class InvalidSpecifier(ValueError): @@ -80,228 +70,15 @@ 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. """ -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) -> ParsedVersion: - if not isinstance(version, (LegacyVersion, 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 or LegacyVersion, 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[VersionTypeVar], prereleases: Optional[bool] = None - ) -> Iterable[VersionTypeVar]: - - 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 - - -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]: - @functools.wraps(fn) - def wrapped(self: "Specifier", prospective: ParsedVersion, spec: str) -> bool: - if not isinstance(prospective, Version): - return False - return fn(self, prospective, spec) - - return wrapped - - -class Specifier(_IndividualSpecifier): +class Specifier(BaseSpecifier): _regex_str = r""" (?P(~=|==|!=|<=|>=|<|>|===)) @@ -341,10 +118,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 .* )? ) | @@ -409,8 +186,60 @@ class Specifier(_IndividualSpecifier): "===": "arbitrary", } - @_require_version_compare - def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool: + 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 _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 @@ -431,8 +260,7 @@ def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool: prospective, prefix ) - @_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(".*"): @@ -471,30 +299,24 @@ 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 # the prospective version. 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 # the prospective version. 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. @@ -519,8 +341,7 @@ def _compare_less_than(self, prospective: ParsedVersion, spec_str: str) -> bool: # version in the spec. 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. @@ -554,6 +375,75 @@ def _compare_greater_than(self, prospective: ParsedVersion, spec_str: str) -> bo 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 = _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 = _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: @@ -574,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 @@ -632,13 +522,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. - parsed: Set[_IndividualSpecifier] = set() + # Specifier. + parsed: Set[Specifier] = 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) @@ -686,7 +573,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 @@ -696,7 +583,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 @@ -731,9 +618,9 @@ def contains( installed: Optional[bool] = None, ) -> bool: - # Ensure that our item is a Version or LegacyVersion instance. - if not isinstance(item, (LegacyVersion, Version)): - item = parse(item) + # Ensure that our item is a Version instance. + if not isinstance(item, Version): + 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 @@ -751,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. @@ -760,8 +647,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,24 +665,13 @@ 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] = [] - - item: UnparsedVersion - parsed_version: Union[Version, LegacyVersion] + filtered: List[UnparsedVersion] = [] + found_prereleases: List[UnparsedVersion] = [] for item in iterable: - # Ensure that we some kind of Version class for this item. - if not isinstance(item, (LegacyVersion, 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 + 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 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_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)") 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"), [ 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