Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add protocol type BaseVersion #400

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Expand Up @@ -4,6 +4,7 @@ omit = packaging/_compat.py

[report]
exclude_lines =
if TYPE_CHECKING:
pragma: no cover
@abc.abstractmethod
@abc.abstractproperty
37 changes: 17 additions & 20 deletions packaging/specifiers.py
Expand Up @@ -21,11 +21,10 @@
)

from .utils import canonicalize_version
from .version import LegacyVersion, Version, parse
from .version import BaseVersion, LegacyVersion, Version, parse

ParsedVersion = Union[Version, LegacyVersion]
UnparsedVersion = Union[Version, LegacyVersion, str]
CallableOperator = Callable[[ParsedVersion, str], bool]
UnparsedVersion = Union[BaseVersion, str]
CallableOperator = Callable[[BaseVersion, str], bool]


class InvalidSpecifier(ValueError):
Expand Down Expand Up @@ -157,8 +156,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) -> BaseVersion:
if isinstance(version, str):
version = parse(version)
return version

Expand Down Expand Up @@ -301,10 +300,10 @@ def _compare_greater_than(self, prospective: LegacyVersion, spec: str) -> bool:


def _require_version_compare(
fn: Callable[["Specifier", ParsedVersion, str], bool]
) -> Callable[["Specifier", ParsedVersion, str], bool]:
fn: Callable[["Specifier", BaseVersion, str], bool]
) -> Callable[["Specifier", BaseVersion, str], bool]:
@functools.wraps(fn)
def wrapped(self: "Specifier", prospective: ParsedVersion, spec: str) -> bool:
def wrapped(self: "Specifier", prospective: BaseVersion, spec: str) -> bool:
if not isinstance(prospective, Version):
return False
return fn(self, prospective, spec)
Expand Down Expand Up @@ -421,7 +420,7 @@ class Specifier(_IndividualSpecifier):
}

@_require_version_compare
def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool:
def _compare_compatible(self, prospective: BaseVersion, 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
Expand All @@ -443,7 +442,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: BaseVersion, spec: str) -> bool:

# We need special logic to handle prefix matching
if spec.endswith(".*"):
Expand Down Expand Up @@ -483,29 +482,27 @@ 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: BaseVersion, 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: BaseVersion, 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: BaseVersion, 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: BaseVersion, spec_str: str) -> bool:

# Convert our spec to a Version instance, since we'll want to work with
# it as a version.
Expand All @@ -531,7 +528,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: BaseVersion, spec_str: str) -> bool:

# Convert our spec to a Version instance, since we'll want to work with
# it as a version.
Expand Down Expand Up @@ -748,7 +745,7 @@ def contains(
) -> bool:

# Ensure that our item is a Version or LegacyVersion instance.
if not isinstance(item, (LegacyVersion, Version)):
if isinstance(item, str):
item = parse(item)

# Determine if we're forcing a prerelease or not, if we're not forcing
Expand Down Expand Up @@ -798,7 +795,7 @@ def filter(

for item in iterable:
# Ensure that we some kind of Version class for this item.
if not isinstance(item, (LegacyVersion, Version)):
if isinstance(item, str):
parsed_version = parse(item)
else:
parsed_version = item
Expand Down
77 changes: 68 additions & 9 deletions packaging/version.py
Expand Up @@ -6,7 +6,17 @@
import itertools
import re
import warnings
from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
Iterator,
List,
Optional,
SupportsInt,
Tuple,
Union,
)

from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType

Expand Down Expand Up @@ -38,8 +48,57 @@
"_Version", ["epoch", "release", "dev", "pre", "post", "local"]
)

# TODO: Remove TYPE_CHECKING guard after dropping Python 3.7 support.
if TYPE_CHECKING:
from typing import Protocol

class BaseVersion(Protocol):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to type __init__() here to make sure implementations don't accidentally cause Liskov violations?

@property
def public(self) -> str:
...

@property
def base_version(self) -> str:
...

@property
def epoch(self) -> int:
...

@property
def pre(self) -> Optional[Tuple[str, int]]:
...

@property
def post(self) -> Optional[int]:
...

@property
def dev(self) -> Optional[int]:
...

@property
def local(self) -> Optional[str]:
...

@property
def is_prerelease(self) -> bool:
...

def parse(version: str) -> Union["LegacyVersion", "Version"]:
@property
def is_postrelease(self) -> bool:
...

@property
def is_devrelease(self) -> bool:
...


else:
BaseVersion = Any


def parse(version: str) -> BaseVersion:
"""
Parse the given version string and return either a :class:`Version` object
or a :class:`LegacyVersion` object depending on if the given version is
Expand All @@ -66,13 +125,13 @@ def __hash__(self) -> int:
# Please keep the duplicated `isinstance` check
# in the six comparisons hereunder
# unless you find a way to avoid adding overhead function calls.
def __lt__(self, other: "_BaseVersion") -> bool:
def __lt__(self, other: BaseVersion) -> bool:
if not isinstance(other, _BaseVersion):
return NotImplemented

return self._key < other._key

def __le__(self, other: "_BaseVersion") -> bool:
def __le__(self, other: BaseVersion) -> bool:
if not isinstance(other, _BaseVersion):
return NotImplemented

Expand All @@ -84,13 +143,13 @@ def __eq__(self, other: object) -> bool:

return self._key == other._key

def __ge__(self, other: "_BaseVersion") -> bool:
def __ge__(self, other: BaseVersion) -> bool:
if not isinstance(other, _BaseVersion):
return NotImplemented

return self._key >= other._key

def __gt__(self, other: "_BaseVersion") -> bool:
def __gt__(self, other: BaseVersion) -> bool:
if not isinstance(other, _BaseVersion):
return NotImplemented

Expand Down Expand Up @@ -141,15 +200,15 @@ def pre(self) -> None:
return None

@property
def post(self) -> None:
def post(self) -> Optional[int]:
return None

@property
def dev(self) -> None:
def dev(self) -> Optional[int]:
return None

@property
def local(self) -> None:
def local(self) -> Optional[str]:
return None

@property
Expand Down