Imported Upstream version 49.4.0 upstream/49.4.0
authorDongHun Kwak <dh0128.kwak@samsung.com>
Tue, 29 Dec 2020 22:08:02 +0000 (07:08 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Tue, 29 Dec 2020 22:08:02 +0000 (07:08 +0900)
27 files changed:
.bumpversion.cfg
CHANGES.rst
pkg_resources/_vendor/packaging/__about__.py
pkg_resources/_vendor/packaging/_compat.py
pkg_resources/_vendor/packaging/_structures.py
pkg_resources/_vendor/packaging/_typing.py [new file with mode: 0644]
pkg_resources/_vendor/packaging/markers.py
pkg_resources/_vendor/packaging/py.typed [new file with mode: 0644]
pkg_resources/_vendor/packaging/requirements.py
pkg_resources/_vendor/packaging/specifiers.py
pkg_resources/_vendor/packaging/tags.py
pkg_resources/_vendor/packaging/utils.py
pkg_resources/_vendor/packaging/version.py
pkg_resources/_vendor/vendored.txt
setup.cfg
setuptools/_vendor/packaging/__about__.py
setuptools/_vendor/packaging/_compat.py
setuptools/_vendor/packaging/_structures.py
setuptools/_vendor/packaging/_typing.py [new file with mode: 0644]
setuptools/_vendor/packaging/markers.py
setuptools/_vendor/packaging/py.typed [new file with mode: 0644]
setuptools/_vendor/packaging/requirements.py
setuptools/_vendor/packaging/specifiers.py
setuptools/_vendor/packaging/tags.py
setuptools/_vendor/packaging/utils.py
setuptools/_vendor/packaging/version.py
setuptools/_vendor/vendored.txt

index 5e0d7fd7a6ad1bf611ae6476662bb384198848de..547dc886cd03f3e69fd000aee6d4a14376bd4463 100644 (file)
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 49.3.2
+current_version = 49.4.0
 commit = True
 tag = True
 
index 2add741a5e3cfdc42f6a490124376b428368f82e..ac07145956179c64c2bbfe23cf021229ae4300d3 100644 (file)
@@ -1,3 +1,9 @@
+v49.4.0
+-------
+
+* #2310: Updated vendored packaging version to 20.4.
+
+
 v49.3.2
 -------
 
@@ -6,7 +12,7 @@ v49.3.2
 
 
 v49.3.1
---------
+-------
 
 * #2316: Removed warning when ``distutils`` is imported before ``setuptools`` when ``distutils`` replacement is not enabled.
 
index dc95138d049ba3194964d528b552a6d1514fa382..4d998578d7b5d39ae1cd5ce8832e7cd85ed2a1d1 100644 (file)
@@ -18,10 +18,10 @@ __title__ = "packaging"
 __summary__ = "Core utilities for Python packages"
 __uri__ = "https://github.com/pypa/packaging"
 
-__version__ = "19.2"
+__version__ = "20.4"
 
 __author__ = "Donald Stufft and individual contributors"
 __email__ = "donald@stufft.io"
 
-__license__ = "BSD or Apache License, Version 2.0"
+__license__ = "BSD-2-Clause or Apache-2.0"
 __copyright__ = "Copyright 2014-2019 %s" % __author__
index 25da473c196855ad59a6d2d785ef1ddef49795be..e54bd4ede8761df5882a3354bc22bdee7a5e8a8b 100644 (file)
@@ -5,6 +5,11 @@ from __future__ import absolute_import, division, print_function
 
 import sys
 
+from ._typing import TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Any, Dict, Tuple, Type
+
 
 PY2 = sys.version_info[0] == 2
 PY3 = sys.version_info[0] == 3
@@ -18,14 +23,16 @@ else:
 
 
 def with_metaclass(meta, *bases):
+    # type: (Type[Any], Tuple[Type[Any], ...]) -> Any
     """
     Create a base class with a metaclass.
     """
     # This requires a bit of explanation: the basic idea is to make a dummy
     # metaclass for one level of class instantiation that replaces itself with
     # the actual metaclass.
-    class metaclass(meta):
+    class metaclass(meta):  # type: ignore
         def __new__(cls, name, this_bases, d):
+            # type: (Type[Any], str, Tuple[Any], Dict[Any, Any]) -> Any
             return meta(name, bases, d)
 
     return type.__new__(metaclass, "temporary_class", (), {})
index 68dcca634d8e3f0081bad2f9ae5e653a2942db68..800d5c5588c99dc216cdea5084da440efb641945 100644 (file)
@@ -4,65 +4,83 @@
 from __future__ import absolute_import, division, print_function
 
 
-class Infinity(object):
+class InfinityType(object):
     def __repr__(self):
+        # type: () -> str
         return "Infinity"
 
     def __hash__(self):
+        # type: () -> int
         return hash(repr(self))
 
     def __lt__(self, other):
+        # type: (object) -> bool
         return False
 
     def __le__(self, other):
+        # type: (object) -> bool
         return False
 
     def __eq__(self, other):
+        # type: (object) -> bool
         return isinstance(other, self.__class__)
 
     def __ne__(self, other):
+        # type: (object) -> bool
         return not isinstance(other, self.__class__)
 
     def __gt__(self, other):
+        # type: (object) -> bool
         return True
 
     def __ge__(self, other):
+        # type: (object) -> bool
         return True
 
     def __neg__(self):
+        # type: (object) -> NegativeInfinityType
         return NegativeInfinity
 
 
-Infinity = Infinity()
+Infinity = InfinityType()
 
 
-class NegativeInfinity(object):
+class NegativeInfinityType(object):
     def __repr__(self):
+        # type: () -> str
         return "-Infinity"
 
     def __hash__(self):
+        # type: () -> int
         return hash(repr(self))
 
     def __lt__(self, other):
+        # type: (object) -> bool
         return True
 
     def __le__(self, other):
+        # type: (object) -> bool
         return True
 
     def __eq__(self, other):
+        # type: (object) -> bool
         return isinstance(other, self.__class__)
 
     def __ne__(self, other):
+        # type: (object) -> bool
         return not isinstance(other, self.__class__)
 
     def __gt__(self, other):
+        # type: (object) -> bool
         return False
 
     def __ge__(self, other):
+        # type: (object) -> bool
         return False
 
     def __neg__(self):
+        # type: (object) -> InfinityType
         return Infinity
 
 
-NegativeInfinity = NegativeInfinity()
+NegativeInfinity = NegativeInfinityType()
diff --git a/pkg_resources/_vendor/packaging/_typing.py b/pkg_resources/_vendor/packaging/_typing.py
new file mode 100644 (file)
index 0000000..77a8b91
--- /dev/null
@@ -0,0 +1,48 @@
+"""For neatly implementing static typing in packaging.
+
+`mypy` - the static type analysis tool we use - uses the `typing` module, which
+provides core functionality fundamental to mypy's functioning.
+
+Generally, `typing` would be imported at runtime and used in that fashion -
+it acts as a no-op at runtime and does not have any run-time overhead by
+design.
+
+As it turns out, `typing` is not vendorable - it uses separate sources for
+Python 2/Python 3. Thus, this codebase can not expect it to be present.
+To work around this, mypy allows the typing import to be behind a False-y
+optional to prevent it from running at runtime and type-comments can be used
+to remove the need for the types to be accessible directly during runtime.
+
+This module provides the False-y guard in a nicely named fashion so that a
+curious maintainer can reach here to read this.
+
+In packaging, all static-typing related imports should be guarded as follows:
+
+    from packaging._typing import TYPE_CHECKING
+
+    if TYPE_CHECKING:
+        from typing import ...
+
+Ref: https://github.com/python/mypy/issues/3216
+"""
+
+__all__ = ["TYPE_CHECKING", "cast"]
+
+# The TYPE_CHECKING constant defined by the typing module is False at runtime
+# but True while type checking.
+if False:  # pragma: no cover
+    from typing import TYPE_CHECKING
+else:
+    TYPE_CHECKING = False
+
+# typing's cast syntax requires calling typing.cast at runtime, but we don't
+# want to import typing at runtime. Here, we inform the type checkers that
+# we're importing `typing.cast` as `cast` and re-implement typing.cast's
+# runtime behavior in a block that is ignored by type checkers.
+if TYPE_CHECKING:  # pragma: no cover
+    # not executed at runtime
+    from typing import cast
+else:
+    # executed at runtime
+    def cast(type_, value):  # noqa
+        return value
index 733123fb3220fc5d732848ffe4336f4b61dc2c70..fd1559c10e3ff25da818f893eb5e0a28c6b6d10d 100644 (file)
@@ -13,8 +13,14 @@ from pkg_resources.extern.pyparsing import ZeroOrMore, Group, Forward, QuotedStr
 from pkg_resources.extern.pyparsing import Literal as L  # noqa
 
 from ._compat import string_types
+from ._typing import TYPE_CHECKING
 from .specifiers import Specifier, InvalidSpecifier
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+    Operator = Callable[[str, str], bool]
+
 
 __all__ = [
     "InvalidMarker",
@@ -46,30 +52,37 @@ class UndefinedEnvironmentName(ValueError):
 
 class Node(object):
     def __init__(self, value):
+        # type: (Any) -> None
         self.value = value
 
     def __str__(self):
+        # type: () -> str
         return str(self.value)
 
     def __repr__(self):
+        # type: () -> str
         return "<{0}({1!r})>".format(self.__class__.__name__, str(self))
 
     def serialize(self):
+        # type: () -> str
         raise NotImplementedError
 
 
 class Variable(Node):
     def serialize(self):
+        # type: () -> str
         return str(self)
 
 
 class Value(Node):
     def serialize(self):
+        # type: () -> str
         return '"{0}"'.format(self)
 
 
 class Op(Node):
     def serialize(self):
+        # type: () -> str
         return str(self)
 
 
@@ -85,13 +98,13 @@ VARIABLE = (
     | L("python_version")
     | L("sys_platform")
     | L("os_name")
-    | L("os.name")
+    | L("os.name")  # PEP-345
     | L("sys.platform")  # PEP-345
     | L("platform.version")  # PEP-345
     | L("platform.machine")  # PEP-345
     | L("platform.python_implementation")  # PEP-345
-    | L("python_implementation")  # PEP-345
-    | L("extra")  # undocumented setuptools legacy
+    | L("python_implementation")  # undocumented setuptools legacy
+    | L("extra")  # PEP-508
 )
 ALIASES = {
     "os.name": "os_name",
@@ -131,6 +144,7 @@ MARKER = stringStart + MARKER_EXPR + stringEnd
 
 
 def _coerce_parse_result(results):
+    # type: (Union[ParseResults, List[Any]]) -> List[Any]
     if isinstance(results, ParseResults):
         return [_coerce_parse_result(i) for i in results]
     else:
@@ -138,6 +152,8 @@ def _coerce_parse_result(results):
 
 
 def _format_marker(marker, first=True):
+    # type: (Union[List[str], Tuple[Node, ...], str], Optional[bool]) -> str
+
     assert isinstance(marker, (list, tuple, string_types))
 
     # Sometimes we have a structure like [[...]] which is a single item list
@@ -172,10 +188,11 @@ _operators = {
     "!=": operator.ne,
     ">=": operator.ge,
     ">": operator.gt,
-}
+}  # type: Dict[str, Operator]
 
 
 def _eval_op(lhs, op, rhs):
+    # type: (str, Op, str) -> bool
     try:
         spec = Specifier("".join([op.serialize(), rhs]))
     except InvalidSpecifier:
@@ -183,7 +200,7 @@ def _eval_op(lhs, op, rhs):
     else:
         return spec.contains(lhs)
 
-    oper = _operators.get(op.serialize())
+    oper = _operators.get(op.serialize())  # type: Optional[Operator]
     if oper is None:
         raise UndefinedComparison(
             "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs)
@@ -192,13 +209,18 @@ def _eval_op(lhs, op, rhs):
     return oper(lhs, rhs)
 
 
-_undefined = object()
+class Undefined(object):
+    pass
+
+
+_undefined = Undefined()
 
 
 def _get_env(environment, name):
-    value = environment.get(name, _undefined)
+    # type: (Dict[str, str], str) -> str
+    value = environment.get(name, _undefined)  # type: Union[str, Undefined]
 
-    if value is _undefined:
+    if isinstance(value, Undefined):
         raise UndefinedEnvironmentName(
             "{0!r} does not exist in evaluation environment.".format(name)
         )
@@ -207,7 +229,8 @@ def _get_env(environment, name):
 
 
 def _evaluate_markers(markers, environment):
-    groups = [[]]
+    # type: (List[Any], Dict[str, str]) -> bool
+    groups = [[]]  # type: List[List[bool]]
 
     for marker in markers:
         assert isinstance(marker, (list, tuple, string_types))
@@ -234,6 +257,7 @@ def _evaluate_markers(markers, environment):
 
 
 def format_full_version(info):
+    # type: (sys._version_info) -> str
     version = "{0.major}.{0.minor}.{0.micro}".format(info)
     kind = info.releaselevel
     if kind != "final":
@@ -242,9 +266,13 @@ def format_full_version(info):
 
 
 def default_environment():
+    # type: () -> Dict[str, str]
     if hasattr(sys, "implementation"):
-        iver = format_full_version(sys.implementation.version)
-        implementation_name = sys.implementation.name
+        # Ignoring the `sys.implementation` reference for type checking due to
+        # mypy not liking that the attribute doesn't exist in Python 2.7 when
+        # run with the `--py27` flag.
+        iver = format_full_version(sys.implementation.version)  # type: ignore
+        implementation_name = sys.implementation.name  # type: ignore
     else:
         iver = "0"
         implementation_name = ""
@@ -266,6 +294,7 @@ def default_environment():
 
 class Marker(object):
     def __init__(self, marker):
+        # type: (str) -> None
         try:
             self._markers = _coerce_parse_result(MARKER.parseString(marker))
         except ParseException as e:
@@ -275,12 +304,15 @@ class Marker(object):
             raise InvalidMarker(err_str)
 
     def __str__(self):
+        # type: () -> str
         return _format_marker(self._markers)
 
     def __repr__(self):
+        # type: () -> str
         return "<Marker({0!r})>".format(str(self))
 
     def evaluate(self, environment=None):
+        # type: (Optional[Dict[str, str]]) -> bool
         """Evaluate a marker.
 
         Return the boolean from evaluating the given marker against the
diff --git a/pkg_resources/_vendor/packaging/py.typed b/pkg_resources/_vendor/packaging/py.typed
new file mode 100644 (file)
index 0000000..e69de29
index c3424dcb64a2e092bfa8a815c30e1b1579fc568e..8282a63259ed7a30cfa5f4c269c03a1555ea383b 100644 (file)
@@ -11,9 +11,13 @@ from pkg_resources.extern.pyparsing import ZeroOrMore, Word, Optional, Regex, Co
 from pkg_resources.extern.pyparsing import Literal as L  # noqa
 from pkg_resources.extern.six.moves.urllib import parse as urlparse
 
+from ._typing import TYPE_CHECKING
 from .markers import MARKER_EXPR, Marker
 from .specifiers import LegacySpecifier, Specifier, SpecifierSet
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import List
+
 
 class InvalidRequirement(ValueError):
     """
@@ -89,6 +93,7 @@ class Requirement(object):
     # TODO: Can we normalize the name and extra name?
 
     def __init__(self, requirement_string):
+        # type: (str) -> None
         try:
             req = REQUIREMENT.parseString(requirement_string)
         except ParseException as e:
@@ -116,7 +121,8 @@ class Requirement(object):
         self.marker = req.marker if req.marker else None
 
     def __str__(self):
-        parts = [self.name]
+        # type: () -> str
+        parts = [self.name]  # type: List[str]
 
         if self.extras:
             parts.append("[{0}]".format(",".join(sorted(self.extras))))
@@ -135,4 +141,5 @@ class Requirement(object):
         return "".join(parts)
 
     def __repr__(self):
+        # type: () -> str
         return "<Requirement({0!r})>".format(str(self))
index 743576a080a0af8d0995f307ea6afc645b13ca61..fe09bb1dbb22f7670d33fe4b86ac45e207cc7eb1 100644 (file)
@@ -9,8 +9,27 @@ import itertools
 import re
 
 from ._compat import string_types, with_metaclass
+from ._typing import TYPE_CHECKING
+from .utils import canonicalize_version
 from .version import Version, LegacyVersion, parse
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import (
+        List,
+        Dict,
+        Union,
+        Iterable,
+        Iterator,
+        Optional,
+        Callable,
+        Tuple,
+        FrozenSet,
+    )
+
+    ParsedVersion = Union[Version, LegacyVersion]
+    UnparsedVersion = Union[Version, LegacyVersion, str]
+    CallableOperator = Callable[[ParsedVersion, str], bool]
+
 
 class InvalidSpecifier(ValueError):
     """
@@ -18,9 +37,10 @@ class InvalidSpecifier(ValueError):
     """
 
 
-class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
+class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):  # type: ignore
     @abc.abstractmethod
     def __str__(self):
+        # type: () -> str
         """
         Returns the str representation of this Specifier like object. This
         should be representative of the Specifier itself.
@@ -28,12 +48,14 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @abc.abstractmethod
     def __hash__(self):
+        # type: () -> int
         """
         Returns a hash value for this Specifier like object.
         """
 
     @abc.abstractmethod
     def __eq__(self, other):
+        # type: (object) -> bool
         """
         Returns a boolean representing whether or not the two Specifier like
         objects are equal.
@@ -41,6 +63,7 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @abc.abstractmethod
     def __ne__(self, other):
+        # type: (object) -> bool
         """
         Returns a boolean representing whether or not the two Specifier like
         objects are not equal.
@@ -48,6 +71,7 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @abc.abstractproperty
     def prereleases(self):
+        # type: () -> Optional[bool]
         """
         Returns whether or not pre-releases as a whole are allowed by this
         specifier.
@@ -55,6 +79,7 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @prereleases.setter
     def prereleases(self, value):
+        # type: (bool) -> None
         """
         Sets whether or not pre-releases as a whole are allowed by this
         specifier.
@@ -62,12 +87,14 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @abc.abstractmethod
     def contains(self, item, prereleases=None):
+        # type: (str, Optional[bool]) -> bool
         """
         Determines if the given item is contained within this specifier.
         """
 
     @abc.abstractmethod
     def filter(self, iterable, prereleases=None):
+        # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion]
         """
         Takes an iterable of items and filters them so that only items which
         are contained within this specifier are allowed in it.
@@ -76,19 +103,24 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
 class _IndividualSpecifier(BaseSpecifier):
 
-    _operators = {}
+    _operators = {}  # type: Dict[str, str]
 
     def __init__(self, spec="", prereleases=None):
+        # type: (str, Optional[bool]) -> None
         match = self._regex.search(spec)
         if not match:
             raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec))
 
-        self._spec = (match.group("operator").strip(), match.group("version").strip())
+        self._spec = (
+            match.group("operator").strip(),
+            match.group("version").strip(),
+        )  # type: Tuple[str, str]
 
         # Store whether or not this Specifier should accept prereleases
         self._prereleases = prereleases
 
     def __repr__(self):
+        # type: () -> str
         pre = (
             ", prereleases={0!r}".format(self.prereleases)
             if self._prereleases is not None
@@ -98,26 +130,35 @@ class _IndividualSpecifier(BaseSpecifier):
         return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre)
 
     def __str__(self):
+        # type: () -> str
         return "{0}{1}".format(*self._spec)
 
+    @property
+    def _canonical_spec(self):
+        # type: () -> Tuple[str, Union[Version, str]]
+        return self._spec[0], canonicalize_version(self._spec[1])
+
     def __hash__(self):
-        return hash(self._spec)
+        # type: () -> int
+        return hash(self._canonical_spec)
 
     def __eq__(self, other):
+        # type: (object) -> bool
         if isinstance(other, string_types):
             try:
-                other = self.__class__(other)
+                other = self.__class__(str(other))
             except InvalidSpecifier:
                 return NotImplemented
         elif not isinstance(other, self.__class__):
             return NotImplemented
 
-        return self._spec == other._spec
+        return self._canonical_spec == other._canonical_spec
 
     def __ne__(self, other):
+        # type: (object) -> bool
         if isinstance(other, string_types):
             try:
-                other = self.__class__(other)
+                other = self.__class__(str(other))
             except InvalidSpecifier:
                 return NotImplemented
         elif not isinstance(other, self.__class__):
@@ -126,52 +167,67 @@ class _IndividualSpecifier(BaseSpecifier):
         return self._spec != other._spec
 
     def _get_operator(self, op):
-        return getattr(self, "_compare_{0}".format(self._operators[op]))
+        # type: (str) -> CallableOperator
+        operator_callable = getattr(
+            self, "_compare_{0}".format(self._operators[op])
+        )  # type: CallableOperator
+        return operator_callable
 
     def _coerce_version(self, version):
+        # type: (UnparsedVersion) -> ParsedVersion
         if not isinstance(version, (LegacyVersion, Version)):
             version = parse(version)
         return version
 
     @property
     def operator(self):
+        # type: () -> str
         return self._spec[0]
 
     @property
     def version(self):
+        # type: () -> str
         return self._spec[1]
 
     @property
     def prereleases(self):
+        # type: () -> Optional[bool]
         return self._prereleases
 
     @prereleases.setter
     def prereleases(self, value):
+        # type: (bool) -> None
         self._prereleases = value
 
     def __contains__(self, item):
+        # type: (str) -> bool
         return self.contains(item)
 
     def contains(self, item, prereleases=None):
+        # type: (UnparsedVersion, Optional[bool]) -> 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")
-        item = self._coerce_version(item)
+        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 item.is_prerelease and not 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.
-        return self._get_operator(self.operator)(item, self.version)
+        operator_callable = self._get_operator(self.operator)  # type: CallableOperator
+        return operator_callable(normalized_item, self.version)
 
     def filter(self, iterable, prereleases=None):
+        # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion]
+
         yielded = False
         found_prereleases = []
 
@@ -230,32 +286,43 @@ class LegacySpecifier(_IndividualSpecifier):
     }
 
     def _coerce_version(self, version):
+        # type: (Union[ParsedVersion, str]) -> LegacyVersion
         if not isinstance(version, LegacyVersion):
             version = LegacyVersion(str(version))
         return version
 
     def _compare_equal(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective == self._coerce_version(spec)
 
     def _compare_not_equal(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective != self._coerce_version(spec)
 
     def _compare_less_than_equal(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective <= self._coerce_version(spec)
 
     def _compare_greater_than_equal(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective >= self._coerce_version(spec)
 
     def _compare_less_than(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective < self._coerce_version(spec)
 
     def _compare_greater_than(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective > self._coerce_version(spec)
 
 
-def _require_version_compare(fn):
+def _require_version_compare(
+    fn  # type: (Callable[[Specifier, ParsedVersion, str], bool])
+):
+    # type: (...) -> Callable[[Specifier, ParsedVersion, str], bool]
     @functools.wraps(fn)
     def wrapped(self, prospective, spec):
+        # type: (Specifier, ParsedVersion, str) -> bool
         if not isinstance(prospective, Version):
             return False
         return fn(self, prospective, spec)
@@ -373,6 +440,8 @@ class Specifier(_IndividualSpecifier):
 
     @_require_version_compare
     def _compare_compatible(self, prospective, spec):
+        # type: (ParsedVersion, 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
         # implement this in terms of the other specifiers instead of
@@ -400,56 +469,75 @@ class Specifier(_IndividualSpecifier):
 
     @_require_version_compare
     def _compare_equal(self, prospective, spec):
+        # type: (ParsedVersion, str) -> bool
+
         # We need special logic to handle prefix matching
         if spec.endswith(".*"):
             # In the case of prefix matching we want to ignore local segment.
             prospective = Version(prospective.public)
             # Split the spec out by dots, and pretend that there is an implicit
             # dot in between a release segment and a pre-release segment.
-            spec = _version_split(spec[:-2])  # Remove the trailing .*
+            split_spec = _version_split(spec[:-2])  # Remove the trailing .*
 
             # Split the prospective version out by dots, and pretend that there
             # is an implicit dot in between a release segment and a pre-release
             # segment.
-            prospective = _version_split(str(prospective))
+            split_prospective = _version_split(str(prospective))
 
             # Shorten the prospective version to be the same length as the spec
             # so that we can determine if the specifier is a prefix of the
             # prospective version or not.
-            prospective = prospective[: len(spec)]
+            shortened_prospective = split_prospective[: len(split_spec)]
 
             # Pad out our two sides with zeros so that they both equal the same
             # length.
-            spec, prospective = _pad_version(spec, prospective)
+            padded_spec, padded_prospective = _pad_version(
+                split_spec, shortened_prospective
+            )
+
+            return padded_prospective == padded_spec
         else:
             # Convert our spec string into a Version
-            spec = Version(spec)
+            spec_version = Version(spec)
 
             # If the specifier does not have a local segment, then we want to
             # act as if the prospective version also does not have a local
             # segment.
-            if not spec.local:
+            if not spec_version.local:
                 prospective = Version(prospective.public)
 
-        return prospective == spec
+            return prospective == spec_version
 
     @_require_version_compare
     def _compare_not_equal(self, prospective, spec):
+        # type: (ParsedVersion, str) -> bool
         return not self._compare_equal(prospective, spec)
 
     @_require_version_compare
     def _compare_less_than_equal(self, prospective, spec):
-        return prospective <= Version(spec)
+        # type: (ParsedVersion, 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, spec):
-        return prospective >= Version(spec)
+        # type: (ParsedVersion, 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, spec):
+    def _compare_less_than(self, prospective, spec_str):
+        # type: (ParsedVersion, str) -> bool
+
         # Convert our spec to a Version instance, since we'll want to work with
         # it as a version.
-        spec = Version(spec)
+        spec = Version(spec_str)
 
         # Check to see if the prospective version is less than the spec
         # version. If it's not we can short circuit and just return False now
@@ -471,10 +559,12 @@ class Specifier(_IndividualSpecifier):
         return True
 
     @_require_version_compare
-    def _compare_greater_than(self, prospective, spec):
+    def _compare_greater_than(self, prospective, spec_str):
+        # type: (ParsedVersion, str) -> bool
+
         # Convert our spec to a Version instance, since we'll want to work with
         # it as a version.
-        spec = Version(spec)
+        spec = Version(spec_str)
 
         # Check to see if the prospective version is greater than the spec
         # version. If it's not we can short circuit and just return False now
@@ -502,10 +592,13 @@ class Specifier(_IndividualSpecifier):
         return True
 
     def _compare_arbitrary(self, prospective, spec):
+        # type: (Version, str) -> bool
         return str(prospective).lower() == str(spec).lower()
 
     @property
     def prereleases(self):
+        # type: () -> bool
+
         # If there is an explicit prereleases set for this, then we'll just
         # blindly use that.
         if self._prereleases is not None:
@@ -530,6 +623,7 @@ class Specifier(_IndividualSpecifier):
 
     @prereleases.setter
     def prereleases(self, value):
+        # type: (bool) -> None
         self._prereleases = value
 
 
@@ -537,7 +631,8 @@ _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
 
 
 def _version_split(version):
-    result = []
+    # type: (str) -> List[str]
+    result = []  # type: List[str]
     for item in version.split("."):
         match = _prefix_regex.search(item)
         if match:
@@ -548,6 +643,7 @@ def _version_split(version):
 
 
 def _pad_version(left, right):
+    # type: (List[str], List[str]) -> Tuple[List[str], List[str]]
     left_split, right_split = [], []
 
     # Get the release segment of our versions
@@ -567,14 +663,16 @@ def _pad_version(left, right):
 
 class SpecifierSet(BaseSpecifier):
     def __init__(self, specifiers="", prereleases=None):
-        # Split on , to break each indidivual specifier into it's own item, and
+        # type: (str, Optional[bool]) -> None
+
+        # Split on , to break each individual specifier into it's own item, and
         # strip each item to remove leading/trailing whitespace.
-        specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
+        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()
-        for specifier in specifiers:
+        for specifier in split_specifiers:
             try:
                 parsed.add(Specifier(specifier))
             except InvalidSpecifier:
@@ -588,6 +686,7 @@ class SpecifierSet(BaseSpecifier):
         self._prereleases = prereleases
 
     def __repr__(self):
+        # type: () -> str
         pre = (
             ", prereleases={0!r}".format(self.prereleases)
             if self._prereleases is not None
@@ -597,12 +696,15 @@ class SpecifierSet(BaseSpecifier):
         return "<SpecifierSet({0!r}{1})>".format(str(self), pre)
 
     def __str__(self):
+        # type: () -> str
         return ",".join(sorted(str(s) for s in self._specs))
 
     def __hash__(self):
+        # type: () -> int
         return hash(self._specs)
 
     def __and__(self, other):
+        # type: (Union[SpecifierSet, str]) -> SpecifierSet
         if isinstance(other, string_types):
             other = SpecifierSet(other)
         elif not isinstance(other, SpecifierSet):
@@ -626,9 +728,8 @@ class SpecifierSet(BaseSpecifier):
         return specifier
 
     def __eq__(self, other):
-        if isinstance(other, string_types):
-            other = SpecifierSet(other)
-        elif isinstance(other, _IndividualSpecifier):
+        # type: (object) -> bool
+        if isinstance(other, (string_types, _IndividualSpecifier)):
             other = SpecifierSet(str(other))
         elif not isinstance(other, SpecifierSet):
             return NotImplemented
@@ -636,9 +737,8 @@ class SpecifierSet(BaseSpecifier):
         return self._specs == other._specs
 
     def __ne__(self, other):
-        if isinstance(other, string_types):
-            other = SpecifierSet(other)
-        elif isinstance(other, _IndividualSpecifier):
+        # type: (object) -> bool
+        if isinstance(other, (string_types, _IndividualSpecifier)):
             other = SpecifierSet(str(other))
         elif not isinstance(other, SpecifierSet):
             return NotImplemented
@@ -646,13 +746,17 @@ class SpecifierSet(BaseSpecifier):
         return self._specs != other._specs
 
     def __len__(self):
+        # type: () -> int
         return len(self._specs)
 
     def __iter__(self):
+        # type: () -> Iterator[FrozenSet[_IndividualSpecifier]]
         return iter(self._specs)
 
     @property
     def prereleases(self):
+        # type: () -> Optional[bool]
+
         # If we have been given an explicit prerelease modifier, then we'll
         # pass that through here.
         if self._prereleases is not None:
@@ -670,12 +774,16 @@ class SpecifierSet(BaseSpecifier):
 
     @prereleases.setter
     def prereleases(self, value):
+        # type: (bool) -> None
         self._prereleases = value
 
     def __contains__(self, item):
+        # type: (Union[ParsedVersion, str]) -> bool
         return self.contains(item)
 
     def contains(self, item, prereleases=None):
+        # type: (Union[ParsedVersion, str], Optional[bool]) -> bool
+
         # Ensure that our item is a Version or LegacyVersion instance.
         if not isinstance(item, (LegacyVersion, Version)):
             item = parse(item)
@@ -701,7 +809,13 @@ class SpecifierSet(BaseSpecifier):
         #       will always return True, this is an explicit design decision.
         return all(s.contains(item, prereleases=prereleases) for s in self._specs)
 
-    def filter(self, iterable, prereleases=None):
+    def filter(
+        self,
+        iterable,  # type: Iterable[Union[ParsedVersion, str]]
+        prereleases=None,  # type: Optional[bool]
+    ):
+        # type: (...) -> Iterable[Union[ParsedVersion, str]]
+
         # 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
         # SpecifierSet thinks for whether or not we should support prereleases.
@@ -719,8 +833,8 @@ class SpecifierSet(BaseSpecifier):
         # which will filter out any pre-releases, unless there are no final
         # releases, and which will filter out LegacyVersion in general.
         else:
-            filtered = []
-            found_prereleases = []
+            filtered = []  # type: List[Union[ParsedVersion, str]]
+            found_prereleases = []  # type: List[Union[ParsedVersion, str]]
 
             for item in iterable:
                 # Ensure that we some kind of Version class for this item.
index ec9942f0f6627f34554082a8c0909bc70bd2a260..9064910b8bafe2d60ce5fca8897226f5e0fb8f8f 100644 (file)
@@ -13,12 +13,37 @@ except ImportError:  # pragma: no cover
 
     EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()]
     del imp
+import logging
+import os
 import platform
 import re
+import struct
 import sys
 import sysconfig
 import warnings
 
+from ._typing import TYPE_CHECKING, cast
+
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import (
+        Dict,
+        FrozenSet,
+        IO,
+        Iterable,
+        Iterator,
+        List,
+        Optional,
+        Sequence,
+        Tuple,
+        Union,
+    )
+
+    PythonVersion = Sequence[int]
+    MacVersion = Tuple[int, int]
+    GlibcVersion = Tuple[int, int]
+
+
+logger = logging.getLogger(__name__)
 
 INTERPRETER_SHORT_NAMES = {
     "python": "py",  # Generic.
@@ -26,34 +51,48 @@ INTERPRETER_SHORT_NAMES = {
     "pypy": "pp",
     "ironpython": "ip",
     "jython": "jy",
-}
+}  # type: Dict[str, str]
 
 
 _32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32
 
 
 class Tag(object):
+    """
+    A representation of the tag triple for a wheel.
+
+    Instances are considered immutable and thus are hashable. Equality checking
+    is also supported.
+    """
 
     __slots__ = ["_interpreter", "_abi", "_platform"]
 
     def __init__(self, interpreter, abi, platform):
+        # type: (str, str, str) -> None
         self._interpreter = interpreter.lower()
         self._abi = abi.lower()
         self._platform = platform.lower()
 
     @property
     def interpreter(self):
+        # type: () -> str
         return self._interpreter
 
     @property
     def abi(self):
+        # type: () -> str
         return self._abi
 
     @property
     def platform(self):
+        # type: () -> str
         return self._platform
 
     def __eq__(self, other):
+        # type: (object) -> bool
+        if not isinstance(other, Tag):
+            return NotImplemented
+
         return (
             (self.platform == other.platform)
             and (self.abi == other.abi)
@@ -61,16 +100,26 @@ class Tag(object):
         )
 
     def __hash__(self):
+        # type: () -> int
         return hash((self._interpreter, self._abi, self._platform))
 
     def __str__(self):
+        # type: () -> str
         return "{}-{}-{}".format(self._interpreter, self._abi, self._platform)
 
     def __repr__(self):
+        # type: () -> str
         return "<{self} @ {self_id}>".format(self=self, self_id=id(self))
 
 
 def parse_tag(tag):
+    # type: (str) -> FrozenSet[Tag]
+    """
+    Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
+
+    Returning a set is required due to the possibility that the tag is a
+    compressed tag set.
+    """
     tags = set()
     interpreters, abis, platforms = tag.split("-")
     for interpreter in interpreters.split("."):
@@ -80,20 +129,54 @@ def parse_tag(tag):
     return frozenset(tags)
 
 
+def _warn_keyword_parameter(func_name, kwargs):
+    # type: (str, Dict[str, bool]) -> bool
+    """
+    Backwards-compatibility with Python 2.7 to allow treating 'warn' as keyword-only.
+    """
+    if not kwargs:
+        return False
+    elif len(kwargs) > 1 or "warn" not in kwargs:
+        kwargs.pop("warn", None)
+        arg = next(iter(kwargs.keys()))
+        raise TypeError(
+            "{}() got an unexpected keyword argument {!r}".format(func_name, arg)
+        )
+    return kwargs["warn"]
+
+
+def _get_config_var(name, warn=False):
+    # type: (str, bool) -> Union[int, str, None]
+    value = sysconfig.get_config_var(name)
+    if value is None and warn:
+        logger.debug(
+            "Config variable '%s' is unset, Python ABI tag may be incorrect", name
+        )
+    return value
+
+
 def _normalize_string(string):
+    # type: (str) -> str
     return string.replace(".", "_").replace("-", "_")
 
 
-def _cpython_interpreter(py_version):
-    # TODO: Is using py_version_nodot for interpreter version critical?
-    return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1])
+def _abi3_applies(python_version):
+    # type: (PythonVersion) -> bool
+    """
+    Determine if the Python version supports abi3.
+
+    PEP 384 was first implemented in Python 3.2.
+    """
+    return len(python_version) > 1 and tuple(python_version) >= (3, 2)
 
 
-def _cpython_abis(py_version):
+def _cpython_abis(py_version, warn=False):
+    # type: (PythonVersion, bool) -> List[str]
+    py_version = tuple(py_version)  # To allow for version comparison.
     abis = []
-    version = "{}{}".format(*py_version[:2])
+    version = _version_nodot(py_version[:2])
     debug = pymalloc = ucs4 = ""
-    with_debug = sysconfig.get_config_var("Py_DEBUG")
+    with_debug = _get_config_var("Py_DEBUG", warn)
     has_refcount = hasattr(sys, "gettotalrefcount")
     # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled
     # extension modules is the best option.
@@ -102,11 +185,11 @@ def _cpython_abis(py_version):
     if with_debug or (with_debug is None and (has_refcount or has_ext)):
         debug = "d"
     if py_version < (3, 8):
-        with_pymalloc = sysconfig.get_config_var("WITH_PYMALLOC")
+        with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
         if with_pymalloc or with_pymalloc is None:
             pymalloc = "m"
         if py_version < (3, 3):
-            unicode_size = sysconfig.get_config_var("Py_UNICODE_SIZE")
+            unicode_size = _get_config_var("Py_UNICODE_SIZE", warn)
             if unicode_size == 4 or (
                 unicode_size is None and sys.maxunicode == 0x10FFFF
             ):
@@ -124,86 +207,148 @@ def _cpython_abis(py_version):
     return abis
 
 
-def _cpython_tags(py_version, interpreter, abis, platforms):
+def cpython_tags(
+    python_version=None,  # type: Optional[PythonVersion]
+    abis=None,  # type: Optional[Iterable[str]]
+    platforms=None,  # type: Optional[Iterable[str]]
+    **kwargs  # type: bool
+):
+    # type: (...) -> Iterator[Tag]
+    """
+    Yields the tags for a CPython interpreter.
+
+    The tags consist of:
+    - cp<python_version>-<abi>-<platform>
+    - cp<python_version>-abi3-<platform>
+    - cp<python_version>-none-<platform>
+    - cp<less than python_version>-abi3-<platform>  # Older Python versions down to 3.2.
+
+    If python_version only specifies a major version then user-provided ABIs and
+    the 'none' ABItag will be used.
+
+    If 'abi3' or 'none' are specified in 'abis' then they will be yielded at
+    their normal position and not at the beginning.
+    """
+    warn = _warn_keyword_parameter("cpython_tags", kwargs)
+    if not python_version:
+        python_version = sys.version_info[:2]
+
+    interpreter = "cp{}".format(_version_nodot(python_version[:2]))
+
+    if abis is None:
+        if len(python_version) > 1:
+            abis = _cpython_abis(python_version, warn)
+        else:
+            abis = []
+    abis = list(abis)
+    # 'abi3' and 'none' are explicitly handled later.
+    for explicit_abi in ("abi3", "none"):
+        try:
+            abis.remove(explicit_abi)
+        except ValueError:
+            pass
+
+    platforms = list(platforms or _platform_tags())
     for abi in abis:
         for platform_ in platforms:
             yield Tag(interpreter, abi, platform_)
-    for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms):
-        yield tag
+    if _abi3_applies(python_version):
+        for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms):
+            yield tag
     for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms):
         yield tag
-    # PEP 384 was first implemented in Python 3.2.
-    for minor_version in range(py_version[1] - 1, 1, -1):
-        for platform_ in platforms:
-            interpreter = "cp{major}{minor}".format(
-                major=py_version[0], minor=minor_version
-            )
-            yield Tag(interpreter, "abi3", platform_)
-
 
-def _pypy_interpreter():
-    return "pp{py_major}{pypy_major}{pypy_minor}".format(
-        py_major=sys.version_info[0],
-        pypy_major=sys.pypy_version_info.major,
-        pypy_minor=sys.pypy_version_info.minor,
-    )
+    if _abi3_applies(python_version):
+        for minor_version in range(python_version[1] - 1, 1, -1):
+            for platform_ in platforms:
+                interpreter = "cp{version}".format(
+                    version=_version_nodot((python_version[0], minor_version))
+                )
+                yield Tag(interpreter, "abi3", platform_)
 
 
 def _generic_abi():
+    # type: () -> Iterator[str]
     abi = sysconfig.get_config_var("SOABI")
     if abi:
-        return _normalize_string(abi)
-    else:
-        return "none"
+        yield _normalize_string(abi)
 
 
-def _pypy_tags(py_version, interpreter, abi, platforms):
-    for tag in (Tag(interpreter, abi, platform) for platform in platforms):
-        yield tag
-    for tag in (Tag(interpreter, "none", platform) for platform in platforms):
-        yield tag
+def generic_tags(
+    interpreter=None,  # type: Optional[str]
+    abis=None,  # type: Optional[Iterable[str]]
+    platforms=None,  # type: Optional[Iterable[str]]
+    **kwargs  # type: bool
+):
+    # type: (...) -> Iterator[Tag]
+    """
+    Yields the tags for a generic interpreter.
 
+    The tags consist of:
+    - <interpreter>-<abi>-<platform>
 
-def _generic_tags(interpreter, py_version, abi, platforms):
-    for tag in (Tag(interpreter, abi, platform) for platform in platforms):
-        yield tag
-    if abi != "none":
-        tags = (Tag(interpreter, "none", platform_) for platform_ in platforms)
-        for tag in tags:
-            yield tag
+    The "none" ABI will be added if it was not explicitly provided.
+    """
+    warn = _warn_keyword_parameter("generic_tags", kwargs)
+    if not interpreter:
+        interp_name = interpreter_name()
+        interp_version = interpreter_version(warn=warn)
+        interpreter = "".join([interp_name, interp_version])
+    if abis is None:
+        abis = _generic_abi()
+    platforms = list(platforms or _platform_tags())
+    abis = list(abis)
+    if "none" not in abis:
+        abis.append("none")
+    for abi in abis:
+        for platform_ in platforms:
+            yield Tag(interpreter, abi, platform_)
 
 
 def _py_interpreter_range(py_version):
+    # type: (PythonVersion) -> Iterator[str]
     """
-    Yield Python versions in descending order.
+    Yields Python versions in descending order.
 
     After the latest version, the major-only version will be yielded, and then
-    all following versions up to 'end'.
+    all previous versions of that major version.
     """
-    yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1])
+    if len(py_version) > 1:
+        yield "py{version}".format(version=_version_nodot(py_version[:2]))
     yield "py{major}".format(major=py_version[0])
-    for minor in range(py_version[1] - 1, -1, -1):
-        yield "py{major}{minor}".format(major=py_version[0], minor=minor)
+    if len(py_version) > 1:
+        for minor in range(py_version[1] - 1, -1, -1):
+            yield "py{version}".format(version=_version_nodot((py_version[0], minor)))
 
 
-def _independent_tags(interpreter, py_version, platforms):
+def compatible_tags(
+    python_version=None,  # type: Optional[PythonVersion]
+    interpreter=None,  # type: Optional[str]
+    platforms=None,  # type: Optional[Iterable[str]]
+):
+    # type: (...) -> Iterator[Tag]
     """
-    Return the sequence of tags that are consistent across implementations.
+    Yields the sequence of tags that are compatible with a specific version of Python.
 
     The tags consist of:
     - py*-none-<platform>
-    - <interpreter>-none-any
+    - <interpreter>-none-any  # ... if `interpreter` is provided.
     - py*-none-any
     """
-    for version in _py_interpreter_range(py_version):
+    if not python_version:
+        python_version = sys.version_info[:2]
+    platforms = list(platforms or _platform_tags())
+    for version in _py_interpreter_range(python_version):
         for platform_ in platforms:
             yield Tag(version, "none", platform_)
-    yield Tag(interpreter, "none", "any")
-    for version in _py_interpreter_range(py_version):
+    if interpreter:
+        yield Tag(interpreter, "none", "any")
+    for version in _py_interpreter_range(python_version):
         yield Tag(version, "none", "any")
 
 
 def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER):
+    # type: (str, bool) -> str
     if not is_32bit:
         return arch
 
@@ -214,6 +359,7 @@ def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER):
 
 
 def _mac_binary_formats(version, cpu_arch):
+    # type: (MacVersion, str) -> List[str]
     formats = [cpu_arch]
     if cpu_arch == "x86_64":
         if version < (10, 4):
@@ -240,32 +386,42 @@ def _mac_binary_formats(version, cpu_arch):
     return formats
 
 
-def _mac_platforms(version=None, arch=None):
-    version_str, _, cpu_arch = platform.mac_ver()
+def mac_platforms(version=None, arch=None):
+    # type: (Optional[MacVersion], Optional[str]) -> Iterator[str]
+    """
+    Yields the platform tags for a macOS system.
+
+    The `version` parameter is a two-item tuple specifying the macOS version to
+    generate platform tags for. The `arch` parameter is the CPU architecture to
+    generate platform tags for. Both parameters default to the appropriate value
+    for the current system.
+    """
+    version_str, _, cpu_arch = platform.mac_ver()  # type: ignore
     if version is None:
-        version = tuple(map(int, version_str.split(".")[:2]))
+        version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
+    else:
+        version = version
     if arch is None:
         arch = _mac_arch(cpu_arch)
-    platforms = []
+    else:
+        arch = arch
     for minor_version in range(version[1], -1, -1):
         compat_version = version[0], minor_version
         binary_formats = _mac_binary_formats(compat_version, arch)
         for binary_format in binary_formats:
-            platforms.append(
-                "macosx_{major}_{minor}_{binary_format}".format(
-                    major=compat_version[0],
-                    minor=compat_version[1],
-                    binary_format=binary_format,
-                )
+            yield "macosx_{major}_{minor}_{binary_format}".format(
+                major=compat_version[0],
+                minor=compat_version[1],
+                binary_format=binary_format,
             )
-    return platforms
 
 
 # From PEP 513.
 def _is_manylinux_compatible(name, glibc_version):
+    # type: (str, GlibcVersion) -> bool
     # Check for presence of _manylinux module.
     try:
-        import _manylinux
+        import _manylinux  # noqa
 
         return bool(getattr(_manylinux, name + "_compatible"))
     except (ImportError, AttributeError):
@@ -276,14 +432,50 @@ def _is_manylinux_compatible(name, glibc_version):
 
 
 def _glibc_version_string():
+    # type: () -> Optional[str]
     # Returns glibc version string, or None if not using glibc.
-    import ctypes
+    return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
+
+
+def _glibc_version_string_confstr():
+    # type: () -> Optional[str]
+    """
+    Primary implementation of glibc_version_string using os.confstr.
+    """
+    # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
+    # to be broken or missing. This strategy is used in the standard library
+    # platform module.
+    # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183
+    try:
+        # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17".
+        version_string = os.confstr(  # type: ignore[attr-defined] # noqa: F821
+            "CS_GNU_LIBC_VERSION"
+        )
+        assert version_string is not None
+        _, version = version_string.split()  # type: Tuple[str, str]
+    except (AssertionError, AttributeError, OSError, ValueError):
+        # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
+        return None
+    return version
+
+
+def _glibc_version_string_ctypes():
+    # type: () -> Optional[str]
+    """
+    Fallback implementation of glibc_version_string using ctypes.
+    """
+    try:
+        import ctypes
+    except ImportError:
+        return None
 
     # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
     # manpage says, "If filename is NULL, then the returned handle is for the
     # main program". This way we can let the linker do the work to figure out
     # which libc our process is actually using.
-    process_namespace = ctypes.CDLL(None)
+    #
+    # Note: typeshed is wrong here so we are ignoring this line.
+    process_namespace = ctypes.CDLL(None)  # type: ignore
     try:
         gnu_get_libc_version = process_namespace.gnu_get_libc_version
     except AttributeError:
@@ -293,7 +485,7 @@ def _glibc_version_string():
 
     # Call gnu_get_libc_version, which returns a string like "2.5"
     gnu_get_libc_version.restype = ctypes.c_char_p
-    version_str = gnu_get_libc_version()
+    version_str = gnu_get_libc_version()  # type: str
     # py2 / py3 compatibility:
     if not isinstance(version_str, str):
         version_str = version_str.decode("ascii")
@@ -303,6 +495,7 @@ def _glibc_version_string():
 
 # Separated out from have_compatible_glibc for easier unit testing.
 def _check_glibc_version(version_str, required_major, minimum_minor):
+    # type: (str, int, int) -> bool
     # Parse string and check against requested version.
     #
     # We use a regexp instead of str.split because we want to discard any
@@ -324,81 +517,235 @@ def _check_glibc_version(version_str, required_major, minimum_minor):
 
 
 def _have_compatible_glibc(required_major, minimum_minor):
+    # type: (int, int) -> bool
     version_str = _glibc_version_string()
     if version_str is None:
         return False
     return _check_glibc_version(version_str, required_major, minimum_minor)
 
 
+# Python does not provide platform information at sufficient granularity to
+# identify the architecture of the running executable in some cases, so we
+# determine it dynamically by reading the information from the running
+# process. This only applies on Linux, which uses the ELF format.
+class _ELFFileHeader(object):
+    # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
+    class _InvalidELFFileHeader(ValueError):
+        """
+        An invalid ELF file header was found.
+        """
+
+    ELF_MAGIC_NUMBER = 0x7F454C46
+    ELFCLASS32 = 1
+    ELFCLASS64 = 2
+    ELFDATA2LSB = 1
+    ELFDATA2MSB = 2
+    EM_386 = 3
+    EM_S390 = 22
+    EM_ARM = 40
+    EM_X86_64 = 62
+    EF_ARM_ABIMASK = 0xFF000000
+    EF_ARM_ABI_VER5 = 0x05000000
+    EF_ARM_ABI_FLOAT_HARD = 0x00000400
+
+    def __init__(self, file):
+        # type: (IO[bytes]) -> None
+        def unpack(fmt):
+            # type: (str) -> int
+            try:
+                (result,) = struct.unpack(
+                    fmt, file.read(struct.calcsize(fmt))
+                )  # type: (int, )
+            except struct.error:
+                raise _ELFFileHeader._InvalidELFFileHeader()
+            return result
+
+        self.e_ident_magic = unpack(">I")
+        if self.e_ident_magic != self.ELF_MAGIC_NUMBER:
+            raise _ELFFileHeader._InvalidELFFileHeader()
+        self.e_ident_class = unpack("B")
+        if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}:
+            raise _ELFFileHeader._InvalidELFFileHeader()
+        self.e_ident_data = unpack("B")
+        if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}:
+            raise _ELFFileHeader._InvalidELFFileHeader()
+        self.e_ident_version = unpack("B")
+        self.e_ident_osabi = unpack("B")
+        self.e_ident_abiversion = unpack("B")
+        self.e_ident_pad = file.read(7)
+        format_h = "<H" if self.e_ident_data == self.ELFDATA2LSB else ">H"
+        format_i = "<I" if self.e_ident_data == self.ELFDATA2LSB else ">I"
+        format_q = "<Q" if self.e_ident_data == self.ELFDATA2LSB else ">Q"
+        format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q
+        self.e_type = unpack(format_h)
+        self.e_machine = unpack(format_h)
+        self.e_version = unpack(format_i)
+        self.e_entry = unpack(format_p)
+        self.e_phoff = unpack(format_p)
+        self.e_shoff = unpack(format_p)
+        self.e_flags = unpack(format_i)
+        self.e_ehsize = unpack(format_h)
+        self.e_phentsize = unpack(format_h)
+        self.e_phnum = unpack(format_h)
+        self.e_shentsize = unpack(format_h)
+        self.e_shnum = unpack(format_h)
+        self.e_shstrndx = unpack(format_h)
+
+
+def _get_elf_header():
+    # type: () -> Optional[_ELFFileHeader]
+    try:
+        with open(sys.executable, "rb") as f:
+            elf_header = _ELFFileHeader(f)
+    except (IOError, OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader):
+        return None
+    return elf_header
+
+
+def _is_linux_armhf():
+    # type: () -> bool
+    # hard-float ABI can be detected from the ELF header of the running
+    # process
+    # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
+    elf_header = _get_elf_header()
+    if elf_header is None:
+        return False
+    result = elf_header.e_ident_class == elf_header.ELFCLASS32
+    result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
+    result &= elf_header.e_machine == elf_header.EM_ARM
+    result &= (
+        elf_header.e_flags & elf_header.EF_ARM_ABIMASK
+    ) == elf_header.EF_ARM_ABI_VER5
+    result &= (
+        elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD
+    ) == elf_header.EF_ARM_ABI_FLOAT_HARD
+    return result
+
+
+def _is_linux_i686():
+    # type: () -> bool
+    elf_header = _get_elf_header()
+    if elf_header is None:
+        return False
+    result = elf_header.e_ident_class == elf_header.ELFCLASS32
+    result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
+    result &= elf_header.e_machine == elf_header.EM_386
+    return result
+
+
+def _have_compatible_manylinux_abi(arch):
+    # type: (str) -> bool
+    if arch == "armv7l":
+        return _is_linux_armhf()
+    if arch == "i686":
+        return _is_linux_i686()
+    return True
+
+
 def _linux_platforms(is_32bit=_32_BIT_INTERPRETER):
+    # type: (bool) -> Iterator[str]
     linux = _normalize_string(distutils.util.get_platform())
-    if linux == "linux_x86_64" and is_32bit:
-        linux = "linux_i686"
-    manylinux_support = (
-        ("manylinux2014", (2, 17)),  # CentOS 7 w/ glibc 2.17 (PEP 599)
-        ("manylinux2010", (2, 12)),  # CentOS 6 w/ glibc 2.12 (PEP 571)
-        ("manylinux1", (2, 5)),  # CentOS 5 w/ glibc 2.5 (PEP 513)
-    )
+    if is_32bit:
+        if linux == "linux_x86_64":
+            linux = "linux_i686"
+        elif linux == "linux_aarch64":
+            linux = "linux_armv7l"
+    manylinux_support = []
+    _, arch = linux.split("_", 1)
+    if _have_compatible_manylinux_abi(arch):
+        if arch in {"x86_64", "i686", "aarch64", "armv7l", "ppc64", "ppc64le", "s390x"}:
+            manylinux_support.append(
+                ("manylinux2014", (2, 17))
+            )  # CentOS 7 w/ glibc 2.17 (PEP 599)
+        if arch in {"x86_64", "i686"}:
+            manylinux_support.append(
+                ("manylinux2010", (2, 12))
+            )  # CentOS 6 w/ glibc 2.12 (PEP 571)
+            manylinux_support.append(
+                ("manylinux1", (2, 5))
+            )  # CentOS 5 w/ glibc 2.5 (PEP 513)
     manylinux_support_iter = iter(manylinux_support)
     for name, glibc_version in manylinux_support_iter:
         if _is_manylinux_compatible(name, glibc_version):
-            platforms = [linux.replace("linux", name)]
+            yield linux.replace("linux", name)
             break
-    else:
-        platforms = []
     # Support for a later manylinux implies support for an earlier version.
-    platforms += [linux.replace("linux", name) for name, _ in manylinux_support_iter]
-    platforms.append(linux)
-    return platforms
+    for name, _ in manylinux_support_iter:
+        yield linux.replace("linux", name)
+    yield linux
 
 
 def _generic_platforms():
-    platform = _normalize_string(distutils.util.get_platform())
-    return [platform]
+    # type: () -> Iterator[str]
+    yield _normalize_string(distutils.util.get_platform())
 
 
-def _interpreter_name():
-    name = platform.python_implementation().lower()
+def _platform_tags():
+    # type: () -> Iterator[str]
+    """
+    Provides the platform tags for this installation.
+    """
+    if platform.system() == "Darwin":
+        return mac_platforms()
+    elif platform.system() == "Linux":
+        return _linux_platforms()
+    else:
+        return _generic_platforms()
+
+
+def interpreter_name():
+    # type: () -> str
+    """
+    Returns the name of the running interpreter.
+    """
+    try:
+        name = sys.implementation.name  # type: ignore
+    except AttributeError:  # pragma: no cover
+        # Python 2.7 compatibility.
+        name = platform.python_implementation().lower()
     return INTERPRETER_SHORT_NAMES.get(name) or name
 
 
-def _generic_interpreter(name, py_version):
-    version = sysconfig.get_config_var("py_version_nodot")
-    if not version:
-        version = "".join(map(str, py_version[:2]))
-    return "{name}{version}".format(name=name, version=version)
+def interpreter_version(**kwargs):
+    # type: (bool) -> str
+    """
+    Returns the version of the running interpreter.
+    """
+    warn = _warn_keyword_parameter("interpreter_version", kwargs)
+    version = _get_config_var("py_version_nodot", warn=warn)
+    if version:
+        version = str(version)
+    else:
+        version = _version_nodot(sys.version_info[:2])
+    return version
+
+
+def _version_nodot(version):
+    # type: (PythonVersion) -> str
+    if any(v >= 10 for v in version):
+        sep = "_"
+    else:
+        sep = ""
+    return sep.join(map(str, version))
 
 
-def sys_tags():
+def sys_tags(**kwargs):
+    # type: (bool) -> Iterator[Tag]
     """
     Returns the sequence of tag triples for the running interpreter.
 
     The order of the sequence corresponds to priority order for the
     interpreter, from most to least important.
     """
-    py_version = sys.version_info[:2]
-    interpreter_name = _interpreter_name()
-    if platform.system() == "Darwin":
-        platforms = _mac_platforms()
-    elif platform.system() == "Linux":
-        platforms = _linux_platforms()
-    else:
-        platforms = _generic_platforms()
+    warn = _warn_keyword_parameter("sys_tags", kwargs)
 
-    if interpreter_name == "cp":
-        interpreter = _cpython_interpreter(py_version)
-        abis = _cpython_abis(py_version)
-        for tag in _cpython_tags(py_version, interpreter, abis, platforms):
-            yield tag
-    elif interpreter_name == "pp":
-        interpreter = _pypy_interpreter()
-        abi = _generic_abi()
-        for tag in _pypy_tags(py_version, interpreter, abi, platforms):
+    interp_name = interpreter_name()
+    if interp_name == "cp":
+        for tag in cpython_tags(warn=warn):
             yield tag
     else:
-        interpreter = _generic_interpreter(interpreter_name, py_version)
-        abi = _generic_abi()
-        for tag in _generic_tags(interpreter, py_version, abi, platforms):
+        for tag in generic_tags():
             yield tag
-    for tag in _independent_tags(interpreter, py_version, platforms):
+
+    for tag in compatible_tags():
         yield tag
index 88418786933b8bc5f6179b8e191f60f79efd7074..19579c1a0fa38c088a7cbb80950d0c85f5514cca 100644 (file)
@@ -5,28 +5,36 @@ from __future__ import absolute_import, division, print_function
 
 import re
 
+from ._typing import TYPE_CHECKING, cast
 from .version import InvalidVersion, Version
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import NewType, Union
+
+    NormalizedName = NewType("NormalizedName", str)
 
 _canonicalize_regex = re.compile(r"[-_.]+")
 
 
 def canonicalize_name(name):
+    # type: (str) -> NormalizedName
     # This is taken from PEP 503.
-    return _canonicalize_regex.sub("-", name).lower()
+    value = _canonicalize_regex.sub("-", name).lower()
+    return cast("NormalizedName", value)
 
 
-def canonicalize_version(version):
+def canonicalize_version(_version):
+    # type: (str) -> Union[Version, str]
     """
-    This is very similar to Version.__str__, but has one subtle differences
+    This is very similar to Version.__str__, but has one subtle difference
     with the way it handles the release segment.
     """
 
     try:
-        version = Version(version)
+        version = Version(_version)
     except InvalidVersion:
         # Legacy versions cannot be normalized
-        return version
+        return _version
 
     parts = []
 
index 95157a1f78c26829ffbe1bd2463f7735b636d16f..00371e86a87edfc5f8d1d1352360bfae0cce8e65 100644 (file)
@@ -7,8 +7,35 @@ import collections
 import itertools
 import re
 
-from ._structures import Infinity
-
+from ._structures import Infinity, NegativeInfinity
+from ._typing import TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union
+
+    from ._structures import InfinityType, NegativeInfinityType
+
+    InfiniteTypes = Union[InfinityType, NegativeInfinityType]
+    PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
+    SubLocalType = Union[InfiniteTypes, int, str]
+    LocalType = Union[
+        NegativeInfinityType,
+        Tuple[
+            Union[
+                SubLocalType,
+                Tuple[SubLocalType, str],
+                Tuple[NegativeInfinityType, SubLocalType],
+            ],
+            ...,
+        ],
+    ]
+    CmpKey = Tuple[
+        int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
+    ]
+    LegacyCmpKey = Tuple[int, Tuple[str, ...]]
+    VersionComparisonMethod = Callable[
+        [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool
+    ]
 
 __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
 
@@ -19,6 +46,7 @@ _Version = collections.namedtuple(
 
 
 def parse(version):
+    # type: (str) -> Union[LegacyVersion, 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
@@ -37,28 +65,38 @@ class InvalidVersion(ValueError):
 
 
 class _BaseVersion(object):
+    _key = None  # type: Union[CmpKey, LegacyCmpKey]
+
     def __hash__(self):
+        # type: () -> int
         return hash(self._key)
 
     def __lt__(self, other):
+        # type: (_BaseVersion) -> bool
         return self._compare(other, lambda s, o: s < o)
 
     def __le__(self, other):
+        # type: (_BaseVersion) -> bool
         return self._compare(other, lambda s, o: s <= o)
 
     def __eq__(self, other):
+        # type: (object) -> bool
         return self._compare(other, lambda s, o: s == o)
 
     def __ge__(self, other):
+        # type: (_BaseVersion) -> bool
         return self._compare(other, lambda s, o: s >= o)
 
     def __gt__(self, other):
+        # type: (_BaseVersion) -> bool
         return self._compare(other, lambda s, o: s > o)
 
     def __ne__(self, other):
+        # type: (object) -> bool
         return self._compare(other, lambda s, o: s != o)
 
     def _compare(self, other, method):
+        # type: (object, VersionComparisonMethod) -> Union[bool, NotImplemented]
         if not isinstance(other, _BaseVersion):
             return NotImplemented
 
@@ -67,57 +105,71 @@ class _BaseVersion(object):
 
 class LegacyVersion(_BaseVersion):
     def __init__(self, version):
+        # type: (str) -> None
         self._version = str(version)
         self._key = _legacy_cmpkey(self._version)
 
     def __str__(self):
+        # type: () -> str
         return self._version
 
     def __repr__(self):
+        # type: () -> str
         return "<LegacyVersion({0})>".format(repr(str(self)))
 
     @property
     def public(self):
+        # type: () -> str
         return self._version
 
     @property
     def base_version(self):
+        # type: () -> str
         return self._version
 
     @property
     def epoch(self):
+        # type: () -> int
         return -1
 
     @property
     def release(self):
+        # type: () -> None
         return None
 
     @property
     def pre(self):
+        # type: () -> None
         return None
 
     @property
     def post(self):
+        # type: () -> None
         return None
 
     @property
     def dev(self):
+        # type: () -> None
         return None
 
     @property
     def local(self):
+        # type: () -> None
         return None
 
     @property
     def is_prerelease(self):
+        # type: () -> bool
         return False
 
     @property
     def is_postrelease(self):
+        # type: () -> bool
         return False
 
     @property
     def is_devrelease(self):
+        # type: () -> bool
         return False
 
 
@@ -133,6 +185,7 @@ _legacy_version_replacement_map = {
 
 
 def _parse_version_parts(s):
+    # type: (str) -> Iterator[str]
     for part in _legacy_version_component_re.split(s):
         part = _legacy_version_replacement_map.get(part, part)
 
@@ -150,6 +203,8 @@ def _parse_version_parts(s):
 
 
 def _legacy_cmpkey(version):
+    # type: (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,
@@ -158,7 +213,7 @@ def _legacy_cmpkey(version):
 
     # This scheme is taken from pkg_resources.parse_version setuptools prior to
     # it's adoption of the packaging library.
-    parts = []
+    parts = []  # type: List[str]
     for part in _parse_version_parts(version.lower()):
         if part.startswith("*"):
             # remove "-" before a prerelease tag
@@ -171,9 +226,8 @@ def _legacy_cmpkey(version):
                 parts.pop()
 
         parts.append(part)
-    parts = tuple(parts)
 
-    return epoch, parts
+    return epoch, tuple(parts)
 
 
 # Deliberately not anchored to the start and end of the string, to make it
@@ -215,6 +269,8 @@ class Version(_BaseVersion):
     _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
 
     def __init__(self, version):
+        # type: (str) -> None
+
         # Validate the version and parse it into pieces
         match = self._regex.search(version)
         if not match:
@@ -243,9 +299,11 @@ class Version(_BaseVersion):
         )
 
     def __repr__(self):
+        # type: () -> str
         return "<Version({0})>".format(repr(str(self)))
 
     def __str__(self):
+        # type: () -> str
         parts = []
 
         # Epoch
@@ -275,26 +333,35 @@ class Version(_BaseVersion):
 
     @property
     def epoch(self):
-        return self._version.epoch
+        # type: () -> int
+        _epoch = self._version.epoch  # type: int
+        return _epoch
 
     @property
     def release(self):
-        return self._version.release
+        # type: () -> Tuple[int, ...]
+        _release = self._version.release  # type: Tuple[int, ...]
+        return _release
 
     @property
     def pre(self):
-        return self._version.pre
+        # type: () -> Optional[Tuple[str, int]]
+        _pre = self._version.pre  # type: Optional[Tuple[str, int]]
+        return _pre
 
     @property
     def post(self):
+        # type: () -> Optional[Tuple[str, int]]
         return self._version.post[1] if self._version.post else None
 
     @property
     def dev(self):
+        # type: () -> Optional[Tuple[str, int]]
         return self._version.dev[1] if self._version.dev else None
 
     @property
     def local(self):
+        # type: () -> Optional[str]
         if self._version.local:
             return ".".join(str(x) for x in self._version.local)
         else:
@@ -302,10 +369,12 @@ class Version(_BaseVersion):
 
     @property
     def public(self):
+        # type: () -> str
         return str(self).split("+", 1)[0]
 
     @property
     def base_version(self):
+        # type: () -> str
         parts = []
 
         # Epoch
@@ -319,18 +388,41 @@ class Version(_BaseVersion):
 
     @property
     def is_prerelease(self):
+        # type: () -> bool
         return self.dev is not None or self.pre is not None
 
     @property
     def is_postrelease(self):
+        # type: () -> bool
         return self.post is not None
 
     @property
     def is_devrelease(self):
+        # type: () -> bool
         return self.dev is not None
 
+    @property
+    def major(self):
+        # type: () -> int
+        return self.release[0] if len(self.release) >= 1 else 0
+
+    @property
+    def minor(self):
+        # type: () -> int
+        return self.release[1] if len(self.release) >= 2 else 0
+
+    @property
+    def micro(self):
+        # type: () -> int
+        return self.release[2] if len(self.release) >= 3 else 0
+
+
+def _parse_letter_version(
+    letter,  # type: str
+    number,  # type: Union[str, bytes, SupportsInt]
+):
+    # type: (...) -> Optional[Tuple[str, int]]
 
-def _parse_letter_version(letter, number):
     if letter:
         # We consider there to be an implicit 0 in a pre-release if there is
         # not a numeral associated with it.
@@ -360,11 +452,14 @@ def _parse_letter_version(letter, number):
 
         return letter, int(number)
 
+    return None
+
 
 _local_version_separators = re.compile(r"[\._-]")
 
 
 def _parse_local_version(local):
+    # type: (str) -> Optional[LocalType]
     """
     Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
     """
@@ -373,15 +468,25 @@ def _parse_local_version(local):
             part.lower() if not part.isdigit() else int(part)
             for part in _local_version_separators.split(local)
         )
+    return None
+
 
+def _cmpkey(
+    epoch,  # type: int
+    release,  # type: Tuple[int, ...]
+    pre,  # type: Optional[Tuple[str, int]]
+    post,  # type: Optional[Tuple[str, int]]
+    dev,  # type: Optional[Tuple[str, int]]
+    local,  # type: Optional[Tuple[SubLocalType]]
+):
+    # type: (...) -> CmpKey
 
-def _cmpkey(epoch, release, pre, post, dev, local):
     # When we compare a release version, we want to compare it with all of the
     # trailing zeros removed. So we'll use a reverse the list, drop all the now
     # leading zeros until we come to something non zero, then take the rest
     # re-reverse it back into the correct order and make it a tuple and use
     # that for our sorting key.
-    release = tuple(
+    _release = tuple(
         reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
     )
 
@@ -390,23 +495,31 @@ def _cmpkey(epoch, release, pre, post, dev, local):
     # if there is not a pre or a post segment. If we have one of those then
     # the normal sorting rules will handle this case correctly.
     if pre is None and post is None and dev is not None:
-        pre = -Infinity
+        _pre = NegativeInfinity  # type: PrePostDevType
     # Versions without a pre-release (except as noted above) should sort after
     # those with one.
     elif pre is None:
-        pre = Infinity
+        _pre = Infinity
+    else:
+        _pre = pre
 
     # Versions without a post segment should sort before those with one.
     if post is None:
-        post = -Infinity
+        _post = NegativeInfinity  # type: PrePostDevType
+
+    else:
+        _post = post
 
     # Versions without a development segment should sort after those with one.
     if dev is None:
-        dev = Infinity
+        _dev = Infinity  # type: PrePostDevType
+
+    else:
+        _dev = dev
 
     if local is None:
         # Versions without a local segment should sort before those with one.
-        local = -Infinity
+        _local = NegativeInfinity  # type: LocalType
     else:
         # Versions with a local segment need that segment parsed to implement
         # the sorting rules in PEP440.
@@ -415,6 +528,8 @@ def _cmpkey(epoch, release, pre, post, dev, local):
         # - Numeric segments sort numerically
         # - Shorter versions sort before longer versions when the prefixes
         #   match exactly
-        local = tuple((i, "") if isinstance(i, int) else (-Infinity, i) for i in local)
+        _local = tuple(
+            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
+        )
 
-    return epoch, release, pre, post, dev, local
+    return epoch, _release, _pre, _post, _dev, _local
index f581a6234914a3d8b81dc18bba2c9057c82e4760..7862a3cead60fb0f82dd0d8e06c3c74e4439ae61 100644 (file)
@@ -1,4 +1,4 @@
-packaging==19.2
+packaging==20.4
 pyparsing==2.2.1
 six==1.10.0
 appdirs==1.4.3
index 5d4a084cdf710237afcd359f79eccdc1d277363e..76d99d44d7f058c45dc8fcb82ffe5574d5e4adc4 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,7 +16,7 @@ formats = zip
 
 [metadata]
 name = setuptools
-version = 49.3.2
+version = 49.4.0
 description = Easily download, build, install, upgrade, and uninstall Python packages
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
index dc95138d049ba3194964d528b552a6d1514fa382..4d998578d7b5d39ae1cd5ce8832e7cd85ed2a1d1 100644 (file)
@@ -18,10 +18,10 @@ __title__ = "packaging"
 __summary__ = "Core utilities for Python packages"
 __uri__ = "https://github.com/pypa/packaging"
 
-__version__ = "19.2"
+__version__ = "20.4"
 
 __author__ = "Donald Stufft and individual contributors"
 __email__ = "donald@stufft.io"
 
-__license__ = "BSD or Apache License, Version 2.0"
+__license__ = "BSD-2-Clause or Apache-2.0"
 __copyright__ = "Copyright 2014-2019 %s" % __author__
index 25da473c196855ad59a6d2d785ef1ddef49795be..e54bd4ede8761df5882a3354bc22bdee7a5e8a8b 100644 (file)
@@ -5,6 +5,11 @@ from __future__ import absolute_import, division, print_function
 
 import sys
 
+from ._typing import TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Any, Dict, Tuple, Type
+
 
 PY2 = sys.version_info[0] == 2
 PY3 = sys.version_info[0] == 3
@@ -18,14 +23,16 @@ else:
 
 
 def with_metaclass(meta, *bases):
+    # type: (Type[Any], Tuple[Type[Any], ...]) -> Any
     """
     Create a base class with a metaclass.
     """
     # This requires a bit of explanation: the basic idea is to make a dummy
     # metaclass for one level of class instantiation that replaces itself with
     # the actual metaclass.
-    class metaclass(meta):
+    class metaclass(meta):  # type: ignore
         def __new__(cls, name, this_bases, d):
+            # type: (Type[Any], str, Tuple[Any], Dict[Any, Any]) -> Any
             return meta(name, bases, d)
 
     return type.__new__(metaclass, "temporary_class", (), {})
index 68dcca634d8e3f0081bad2f9ae5e653a2942db68..800d5c5588c99dc216cdea5084da440efb641945 100644 (file)
@@ -4,65 +4,83 @@
 from __future__ import absolute_import, division, print_function
 
 
-class Infinity(object):
+class InfinityType(object):
     def __repr__(self):
+        # type: () -> str
         return "Infinity"
 
     def __hash__(self):
+        # type: () -> int
         return hash(repr(self))
 
     def __lt__(self, other):
+        # type: (object) -> bool
         return False
 
     def __le__(self, other):
+        # type: (object) -> bool
         return False
 
     def __eq__(self, other):
+        # type: (object) -> bool
         return isinstance(other, self.__class__)
 
     def __ne__(self, other):
+        # type: (object) -> bool
         return not isinstance(other, self.__class__)
 
     def __gt__(self, other):
+        # type: (object) -> bool
         return True
 
     def __ge__(self, other):
+        # type: (object) -> bool
         return True
 
     def __neg__(self):
+        # type: (object) -> NegativeInfinityType
         return NegativeInfinity
 
 
-Infinity = Infinity()
+Infinity = InfinityType()
 
 
-class NegativeInfinity(object):
+class NegativeInfinityType(object):
     def __repr__(self):
+        # type: () -> str
         return "-Infinity"
 
     def __hash__(self):
+        # type: () -> int
         return hash(repr(self))
 
     def __lt__(self, other):
+        # type: (object) -> bool
         return True
 
     def __le__(self, other):
+        # type: (object) -> bool
         return True
 
     def __eq__(self, other):
+        # type: (object) -> bool
         return isinstance(other, self.__class__)
 
     def __ne__(self, other):
+        # type: (object) -> bool
         return not isinstance(other, self.__class__)
 
     def __gt__(self, other):
+        # type: (object) -> bool
         return False
 
     def __ge__(self, other):
+        # type: (object) -> bool
         return False
 
     def __neg__(self):
+        # type: (object) -> InfinityType
         return Infinity
 
 
-NegativeInfinity = NegativeInfinity()
+NegativeInfinity = NegativeInfinityType()
diff --git a/setuptools/_vendor/packaging/_typing.py b/setuptools/_vendor/packaging/_typing.py
new file mode 100644 (file)
index 0000000..77a8b91
--- /dev/null
@@ -0,0 +1,48 @@
+"""For neatly implementing static typing in packaging.
+
+`mypy` - the static type analysis tool we use - uses the `typing` module, which
+provides core functionality fundamental to mypy's functioning.
+
+Generally, `typing` would be imported at runtime and used in that fashion -
+it acts as a no-op at runtime and does not have any run-time overhead by
+design.
+
+As it turns out, `typing` is not vendorable - it uses separate sources for
+Python 2/Python 3. Thus, this codebase can not expect it to be present.
+To work around this, mypy allows the typing import to be behind a False-y
+optional to prevent it from running at runtime and type-comments can be used
+to remove the need for the types to be accessible directly during runtime.
+
+This module provides the False-y guard in a nicely named fashion so that a
+curious maintainer can reach here to read this.
+
+In packaging, all static-typing related imports should be guarded as follows:
+
+    from packaging._typing import TYPE_CHECKING
+
+    if TYPE_CHECKING:
+        from typing import ...
+
+Ref: https://github.com/python/mypy/issues/3216
+"""
+
+__all__ = ["TYPE_CHECKING", "cast"]
+
+# The TYPE_CHECKING constant defined by the typing module is False at runtime
+# but True while type checking.
+if False:  # pragma: no cover
+    from typing import TYPE_CHECKING
+else:
+    TYPE_CHECKING = False
+
+# typing's cast syntax requires calling typing.cast at runtime, but we don't
+# want to import typing at runtime. Here, we inform the type checkers that
+# we're importing `typing.cast` as `cast` and re-implement typing.cast's
+# runtime behavior in a block that is ignored by type checkers.
+if TYPE_CHECKING:  # pragma: no cover
+    # not executed at runtime
+    from typing import cast
+else:
+    # executed at runtime
+    def cast(type_, value):  # noqa
+        return value
index 4bdfdb24f2096eac046bb9a576065bb96cfd476e..03fbdfcc944ef6f969744134957a428d19c743ca 100644 (file)
@@ -13,8 +13,14 @@ from setuptools.extern.pyparsing import ZeroOrMore, Group, Forward, QuotedString
 from setuptools.extern.pyparsing import Literal as L  # noqa
 
 from ._compat import string_types
+from ._typing import TYPE_CHECKING
 from .specifiers import Specifier, InvalidSpecifier
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+    Operator = Callable[[str, str], bool]
+
 
 __all__ = [
     "InvalidMarker",
@@ -46,30 +52,37 @@ class UndefinedEnvironmentName(ValueError):
 
 class Node(object):
     def __init__(self, value):
+        # type: (Any) -> None
         self.value = value
 
     def __str__(self):
+        # type: () -> str
         return str(self.value)
 
     def __repr__(self):
+        # type: () -> str
         return "<{0}({1!r})>".format(self.__class__.__name__, str(self))
 
     def serialize(self):
+        # type: () -> str
         raise NotImplementedError
 
 
 class Variable(Node):
     def serialize(self):
+        # type: () -> str
         return str(self)
 
 
 class Value(Node):
     def serialize(self):
+        # type: () -> str
         return '"{0}"'.format(self)
 
 
 class Op(Node):
     def serialize(self):
+        # type: () -> str
         return str(self)
 
 
@@ -85,13 +98,13 @@ VARIABLE = (
     | L("python_version")
     | L("sys_platform")
     | L("os_name")
-    | L("os.name")
+    | L("os.name")  # PEP-345
     | L("sys.platform")  # PEP-345
     | L("platform.version")  # PEP-345
     | L("platform.machine")  # PEP-345
     | L("platform.python_implementation")  # PEP-345
-    | L("python_implementation")  # PEP-345
-    | L("extra")  # undocumented setuptools legacy
+    | L("python_implementation")  # undocumented setuptools legacy
+    | L("extra")  # PEP-508
 )
 ALIASES = {
     "os.name": "os_name",
@@ -131,6 +144,7 @@ MARKER = stringStart + MARKER_EXPR + stringEnd
 
 
 def _coerce_parse_result(results):
+    # type: (Union[ParseResults, List[Any]]) -> List[Any]
     if isinstance(results, ParseResults):
         return [_coerce_parse_result(i) for i in results]
     else:
@@ -138,6 +152,8 @@ def _coerce_parse_result(results):
 
 
 def _format_marker(marker, first=True):
+    # type: (Union[List[str], Tuple[Node, ...], str], Optional[bool]) -> str
+
     assert isinstance(marker, (list, tuple, string_types))
 
     # Sometimes we have a structure like [[...]] which is a single item list
@@ -172,10 +188,11 @@ _operators = {
     "!=": operator.ne,
     ">=": operator.ge,
     ">": operator.gt,
-}
+}  # type: Dict[str, Operator]
 
 
 def _eval_op(lhs, op, rhs):
+    # type: (str, Op, str) -> bool
     try:
         spec = Specifier("".join([op.serialize(), rhs]))
     except InvalidSpecifier:
@@ -183,7 +200,7 @@ def _eval_op(lhs, op, rhs):
     else:
         return spec.contains(lhs)
 
-    oper = _operators.get(op.serialize())
+    oper = _operators.get(op.serialize())  # type: Optional[Operator]
     if oper is None:
         raise UndefinedComparison(
             "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs)
@@ -192,13 +209,18 @@ def _eval_op(lhs, op, rhs):
     return oper(lhs, rhs)
 
 
-_undefined = object()
+class Undefined(object):
+    pass
+
+
+_undefined = Undefined()
 
 
 def _get_env(environment, name):
-    value = environment.get(name, _undefined)
+    # type: (Dict[str, str], str) -> str
+    value = environment.get(name, _undefined)  # type: Union[str, Undefined]
 
-    if value is _undefined:
+    if isinstance(value, Undefined):
         raise UndefinedEnvironmentName(
             "{0!r} does not exist in evaluation environment.".format(name)
         )
@@ -207,7 +229,8 @@ def _get_env(environment, name):
 
 
 def _evaluate_markers(markers, environment):
-    groups = [[]]
+    # type: (List[Any], Dict[str, str]) -> bool
+    groups = [[]]  # type: List[List[bool]]
 
     for marker in markers:
         assert isinstance(marker, (list, tuple, string_types))
@@ -234,6 +257,7 @@ def _evaluate_markers(markers, environment):
 
 
 def format_full_version(info):
+    # type: (sys._version_info) -> str
     version = "{0.major}.{0.minor}.{0.micro}".format(info)
     kind = info.releaselevel
     if kind != "final":
@@ -242,9 +266,13 @@ def format_full_version(info):
 
 
 def default_environment():
+    # type: () -> Dict[str, str]
     if hasattr(sys, "implementation"):
-        iver = format_full_version(sys.implementation.version)
-        implementation_name = sys.implementation.name
+        # Ignoring the `sys.implementation` reference for type checking due to
+        # mypy not liking that the attribute doesn't exist in Python 2.7 when
+        # run with the `--py27` flag.
+        iver = format_full_version(sys.implementation.version)  # type: ignore
+        implementation_name = sys.implementation.name  # type: ignore
     else:
         iver = "0"
         implementation_name = ""
@@ -266,6 +294,7 @@ def default_environment():
 
 class Marker(object):
     def __init__(self, marker):
+        # type: (str) -> None
         try:
             self._markers = _coerce_parse_result(MARKER.parseString(marker))
         except ParseException as e:
@@ -275,12 +304,15 @@ class Marker(object):
             raise InvalidMarker(err_str)
 
     def __str__(self):
+        # type: () -> str
         return _format_marker(self._markers)
 
     def __repr__(self):
+        # type: () -> str
         return "<Marker({0!r})>".format(str(self))
 
     def evaluate(self, environment=None):
+        # type: (Optional[Dict[str, str]]) -> bool
         """Evaluate a marker.
 
         Return the boolean from evaluating the given marker against the
diff --git a/setuptools/_vendor/packaging/py.typed b/setuptools/_vendor/packaging/py.typed
new file mode 100644 (file)
index 0000000..e69de29
index 8a0c2cb9be06e633b26c7205d6efe42827835910..06b1748851239af81bd2755e11afecf38383eec7 100644 (file)
@@ -11,9 +11,13 @@ from setuptools.extern.pyparsing import ZeroOrMore, Word, Optional, Regex, Combi
 from setuptools.extern.pyparsing import Literal as L  # noqa
 from setuptools.extern.six.moves.urllib import parse as urlparse
 
+from ._typing import TYPE_CHECKING
 from .markers import MARKER_EXPR, Marker
 from .specifiers import LegacySpecifier, Specifier, SpecifierSet
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import List
+
 
 class InvalidRequirement(ValueError):
     """
@@ -89,6 +93,7 @@ class Requirement(object):
     # TODO: Can we normalize the name and extra name?
 
     def __init__(self, requirement_string):
+        # type: (str) -> None
         try:
             req = REQUIREMENT.parseString(requirement_string)
         except ParseException as e:
@@ -116,7 +121,8 @@ class Requirement(object):
         self.marker = req.marker if req.marker else None
 
     def __str__(self):
-        parts = [self.name]
+        # type: () -> str
+        parts = [self.name]  # type: List[str]
 
         if self.extras:
             parts.append("[{0}]".format(",".join(sorted(self.extras))))
@@ -135,4 +141,5 @@ class Requirement(object):
         return "".join(parts)
 
     def __repr__(self):
+        # type: () -> str
         return "<Requirement({0!r})>".format(str(self))
index 743576a080a0af8d0995f307ea6afc645b13ca61..fe09bb1dbb22f7670d33fe4b86ac45e207cc7eb1 100644 (file)
@@ -9,8 +9,27 @@ import itertools
 import re
 
 from ._compat import string_types, with_metaclass
+from ._typing import TYPE_CHECKING
+from .utils import canonicalize_version
 from .version import Version, LegacyVersion, parse
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import (
+        List,
+        Dict,
+        Union,
+        Iterable,
+        Iterator,
+        Optional,
+        Callable,
+        Tuple,
+        FrozenSet,
+    )
+
+    ParsedVersion = Union[Version, LegacyVersion]
+    UnparsedVersion = Union[Version, LegacyVersion, str]
+    CallableOperator = Callable[[ParsedVersion, str], bool]
+
 
 class InvalidSpecifier(ValueError):
     """
@@ -18,9 +37,10 @@ class InvalidSpecifier(ValueError):
     """
 
 
-class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
+class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):  # type: ignore
     @abc.abstractmethod
     def __str__(self):
+        # type: () -> str
         """
         Returns the str representation of this Specifier like object. This
         should be representative of the Specifier itself.
@@ -28,12 +48,14 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @abc.abstractmethod
     def __hash__(self):
+        # type: () -> int
         """
         Returns a hash value for this Specifier like object.
         """
 
     @abc.abstractmethod
     def __eq__(self, other):
+        # type: (object) -> bool
         """
         Returns a boolean representing whether or not the two Specifier like
         objects are equal.
@@ -41,6 +63,7 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @abc.abstractmethod
     def __ne__(self, other):
+        # type: (object) -> bool
         """
         Returns a boolean representing whether or not the two Specifier like
         objects are not equal.
@@ -48,6 +71,7 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @abc.abstractproperty
     def prereleases(self):
+        # type: () -> Optional[bool]
         """
         Returns whether or not pre-releases as a whole are allowed by this
         specifier.
@@ -55,6 +79,7 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @prereleases.setter
     def prereleases(self, value):
+        # type: (bool) -> None
         """
         Sets whether or not pre-releases as a whole are allowed by this
         specifier.
@@ -62,12 +87,14 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
     @abc.abstractmethod
     def contains(self, item, prereleases=None):
+        # type: (str, Optional[bool]) -> bool
         """
         Determines if the given item is contained within this specifier.
         """
 
     @abc.abstractmethod
     def filter(self, iterable, prereleases=None):
+        # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion]
         """
         Takes an iterable of items and filters them so that only items which
         are contained within this specifier are allowed in it.
@@ -76,19 +103,24 @@ class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
 
 class _IndividualSpecifier(BaseSpecifier):
 
-    _operators = {}
+    _operators = {}  # type: Dict[str, str]
 
     def __init__(self, spec="", prereleases=None):
+        # type: (str, Optional[bool]) -> None
         match = self._regex.search(spec)
         if not match:
             raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec))
 
-        self._spec = (match.group("operator").strip(), match.group("version").strip())
+        self._spec = (
+            match.group("operator").strip(),
+            match.group("version").strip(),
+        )  # type: Tuple[str, str]
 
         # Store whether or not this Specifier should accept prereleases
         self._prereleases = prereleases
 
     def __repr__(self):
+        # type: () -> str
         pre = (
             ", prereleases={0!r}".format(self.prereleases)
             if self._prereleases is not None
@@ -98,26 +130,35 @@ class _IndividualSpecifier(BaseSpecifier):
         return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre)
 
     def __str__(self):
+        # type: () -> str
         return "{0}{1}".format(*self._spec)
 
+    @property
+    def _canonical_spec(self):
+        # type: () -> Tuple[str, Union[Version, str]]
+        return self._spec[0], canonicalize_version(self._spec[1])
+
     def __hash__(self):
-        return hash(self._spec)
+        # type: () -> int
+        return hash(self._canonical_spec)
 
     def __eq__(self, other):
+        # type: (object) -> bool
         if isinstance(other, string_types):
             try:
-                other = self.__class__(other)
+                other = self.__class__(str(other))
             except InvalidSpecifier:
                 return NotImplemented
         elif not isinstance(other, self.__class__):
             return NotImplemented
 
-        return self._spec == other._spec
+        return self._canonical_spec == other._canonical_spec
 
     def __ne__(self, other):
+        # type: (object) -> bool
         if isinstance(other, string_types):
             try:
-                other = self.__class__(other)
+                other = self.__class__(str(other))
             except InvalidSpecifier:
                 return NotImplemented
         elif not isinstance(other, self.__class__):
@@ -126,52 +167,67 @@ class _IndividualSpecifier(BaseSpecifier):
         return self._spec != other._spec
 
     def _get_operator(self, op):
-        return getattr(self, "_compare_{0}".format(self._operators[op]))
+        # type: (str) -> CallableOperator
+        operator_callable = getattr(
+            self, "_compare_{0}".format(self._operators[op])
+        )  # type: CallableOperator
+        return operator_callable
 
     def _coerce_version(self, version):
+        # type: (UnparsedVersion) -> ParsedVersion
         if not isinstance(version, (LegacyVersion, Version)):
             version = parse(version)
         return version
 
     @property
     def operator(self):
+        # type: () -> str
         return self._spec[0]
 
     @property
     def version(self):
+        # type: () -> str
         return self._spec[1]
 
     @property
     def prereleases(self):
+        # type: () -> Optional[bool]
         return self._prereleases
 
     @prereleases.setter
     def prereleases(self, value):
+        # type: (bool) -> None
         self._prereleases = value
 
     def __contains__(self, item):
+        # type: (str) -> bool
         return self.contains(item)
 
     def contains(self, item, prereleases=None):
+        # type: (UnparsedVersion, Optional[bool]) -> 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")
-        item = self._coerce_version(item)
+        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 item.is_prerelease and not 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.
-        return self._get_operator(self.operator)(item, self.version)
+        operator_callable = self._get_operator(self.operator)  # type: CallableOperator
+        return operator_callable(normalized_item, self.version)
 
     def filter(self, iterable, prereleases=None):
+        # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion]
+
         yielded = False
         found_prereleases = []
 
@@ -230,32 +286,43 @@ class LegacySpecifier(_IndividualSpecifier):
     }
 
     def _coerce_version(self, version):
+        # type: (Union[ParsedVersion, str]) -> LegacyVersion
         if not isinstance(version, LegacyVersion):
             version = LegacyVersion(str(version))
         return version
 
     def _compare_equal(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective == self._coerce_version(spec)
 
     def _compare_not_equal(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective != self._coerce_version(spec)
 
     def _compare_less_than_equal(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective <= self._coerce_version(spec)
 
     def _compare_greater_than_equal(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective >= self._coerce_version(spec)
 
     def _compare_less_than(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective < self._coerce_version(spec)
 
     def _compare_greater_than(self, prospective, spec):
+        # type: (LegacyVersion, str) -> bool
         return prospective > self._coerce_version(spec)
 
 
-def _require_version_compare(fn):
+def _require_version_compare(
+    fn  # type: (Callable[[Specifier, ParsedVersion, str], bool])
+):
+    # type: (...) -> Callable[[Specifier, ParsedVersion, str], bool]
     @functools.wraps(fn)
     def wrapped(self, prospective, spec):
+        # type: (Specifier, ParsedVersion, str) -> bool
         if not isinstance(prospective, Version):
             return False
         return fn(self, prospective, spec)
@@ -373,6 +440,8 @@ class Specifier(_IndividualSpecifier):
 
     @_require_version_compare
     def _compare_compatible(self, prospective, spec):
+        # type: (ParsedVersion, 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
         # implement this in terms of the other specifiers instead of
@@ -400,56 +469,75 @@ class Specifier(_IndividualSpecifier):
 
     @_require_version_compare
     def _compare_equal(self, prospective, spec):
+        # type: (ParsedVersion, str) -> bool
+
         # We need special logic to handle prefix matching
         if spec.endswith(".*"):
             # In the case of prefix matching we want to ignore local segment.
             prospective = Version(prospective.public)
             # Split the spec out by dots, and pretend that there is an implicit
             # dot in between a release segment and a pre-release segment.
-            spec = _version_split(spec[:-2])  # Remove the trailing .*
+            split_spec = _version_split(spec[:-2])  # Remove the trailing .*
 
             # Split the prospective version out by dots, and pretend that there
             # is an implicit dot in between a release segment and a pre-release
             # segment.
-            prospective = _version_split(str(prospective))
+            split_prospective = _version_split(str(prospective))
 
             # Shorten the prospective version to be the same length as the spec
             # so that we can determine if the specifier is a prefix of the
             # prospective version or not.
-            prospective = prospective[: len(spec)]
+            shortened_prospective = split_prospective[: len(split_spec)]
 
             # Pad out our two sides with zeros so that they both equal the same
             # length.
-            spec, prospective = _pad_version(spec, prospective)
+            padded_spec, padded_prospective = _pad_version(
+                split_spec, shortened_prospective
+            )
+
+            return padded_prospective == padded_spec
         else:
             # Convert our spec string into a Version
-            spec = Version(spec)
+            spec_version = Version(spec)
 
             # If the specifier does not have a local segment, then we want to
             # act as if the prospective version also does not have a local
             # segment.
-            if not spec.local:
+            if not spec_version.local:
                 prospective = Version(prospective.public)
 
-        return prospective == spec
+            return prospective == spec_version
 
     @_require_version_compare
     def _compare_not_equal(self, prospective, spec):
+        # type: (ParsedVersion, str) -> bool
         return not self._compare_equal(prospective, spec)
 
     @_require_version_compare
     def _compare_less_than_equal(self, prospective, spec):
-        return prospective <= Version(spec)
+        # type: (ParsedVersion, 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, spec):
-        return prospective >= Version(spec)
+        # type: (ParsedVersion, 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, spec):
+    def _compare_less_than(self, prospective, spec_str):
+        # type: (ParsedVersion, str) -> bool
+
         # Convert our spec to a Version instance, since we'll want to work with
         # it as a version.
-        spec = Version(spec)
+        spec = Version(spec_str)
 
         # Check to see if the prospective version is less than the spec
         # version. If it's not we can short circuit and just return False now
@@ -471,10 +559,12 @@ class Specifier(_IndividualSpecifier):
         return True
 
     @_require_version_compare
-    def _compare_greater_than(self, prospective, spec):
+    def _compare_greater_than(self, prospective, spec_str):
+        # type: (ParsedVersion, str) -> bool
+
         # Convert our spec to a Version instance, since we'll want to work with
         # it as a version.
-        spec = Version(spec)
+        spec = Version(spec_str)
 
         # Check to see if the prospective version is greater than the spec
         # version. If it's not we can short circuit and just return False now
@@ -502,10 +592,13 @@ class Specifier(_IndividualSpecifier):
         return True
 
     def _compare_arbitrary(self, prospective, spec):
+        # type: (Version, str) -> bool
         return str(prospective).lower() == str(spec).lower()
 
     @property
     def prereleases(self):
+        # type: () -> bool
+
         # If there is an explicit prereleases set for this, then we'll just
         # blindly use that.
         if self._prereleases is not None:
@@ -530,6 +623,7 @@ class Specifier(_IndividualSpecifier):
 
     @prereleases.setter
     def prereleases(self, value):
+        # type: (bool) -> None
         self._prereleases = value
 
 
@@ -537,7 +631,8 @@ _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
 
 
 def _version_split(version):
-    result = []
+    # type: (str) -> List[str]
+    result = []  # type: List[str]
     for item in version.split("."):
         match = _prefix_regex.search(item)
         if match:
@@ -548,6 +643,7 @@ def _version_split(version):
 
 
 def _pad_version(left, right):
+    # type: (List[str], List[str]) -> Tuple[List[str], List[str]]
     left_split, right_split = [], []
 
     # Get the release segment of our versions
@@ -567,14 +663,16 @@ def _pad_version(left, right):
 
 class SpecifierSet(BaseSpecifier):
     def __init__(self, specifiers="", prereleases=None):
-        # Split on , to break each indidivual specifier into it's own item, and
+        # type: (str, Optional[bool]) -> None
+
+        # Split on , to break each individual specifier into it's own item, and
         # strip each item to remove leading/trailing whitespace.
-        specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
+        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()
-        for specifier in specifiers:
+        for specifier in split_specifiers:
             try:
                 parsed.add(Specifier(specifier))
             except InvalidSpecifier:
@@ -588,6 +686,7 @@ class SpecifierSet(BaseSpecifier):
         self._prereleases = prereleases
 
     def __repr__(self):
+        # type: () -> str
         pre = (
             ", prereleases={0!r}".format(self.prereleases)
             if self._prereleases is not None
@@ -597,12 +696,15 @@ class SpecifierSet(BaseSpecifier):
         return "<SpecifierSet({0!r}{1})>".format(str(self), pre)
 
     def __str__(self):
+        # type: () -> str
         return ",".join(sorted(str(s) for s in self._specs))
 
     def __hash__(self):
+        # type: () -> int
         return hash(self._specs)
 
     def __and__(self, other):
+        # type: (Union[SpecifierSet, str]) -> SpecifierSet
         if isinstance(other, string_types):
             other = SpecifierSet(other)
         elif not isinstance(other, SpecifierSet):
@@ -626,9 +728,8 @@ class SpecifierSet(BaseSpecifier):
         return specifier
 
     def __eq__(self, other):
-        if isinstance(other, string_types):
-            other = SpecifierSet(other)
-        elif isinstance(other, _IndividualSpecifier):
+        # type: (object) -> bool
+        if isinstance(other, (string_types, _IndividualSpecifier)):
             other = SpecifierSet(str(other))
         elif not isinstance(other, SpecifierSet):
             return NotImplemented
@@ -636,9 +737,8 @@ class SpecifierSet(BaseSpecifier):
         return self._specs == other._specs
 
     def __ne__(self, other):
-        if isinstance(other, string_types):
-            other = SpecifierSet(other)
-        elif isinstance(other, _IndividualSpecifier):
+        # type: (object) -> bool
+        if isinstance(other, (string_types, _IndividualSpecifier)):
             other = SpecifierSet(str(other))
         elif not isinstance(other, SpecifierSet):
             return NotImplemented
@@ -646,13 +746,17 @@ class SpecifierSet(BaseSpecifier):
         return self._specs != other._specs
 
     def __len__(self):
+        # type: () -> int
         return len(self._specs)
 
     def __iter__(self):
+        # type: () -> Iterator[FrozenSet[_IndividualSpecifier]]
         return iter(self._specs)
 
     @property
     def prereleases(self):
+        # type: () -> Optional[bool]
+
         # If we have been given an explicit prerelease modifier, then we'll
         # pass that through here.
         if self._prereleases is not None:
@@ -670,12 +774,16 @@ class SpecifierSet(BaseSpecifier):
 
     @prereleases.setter
     def prereleases(self, value):
+        # type: (bool) -> None
         self._prereleases = value
 
     def __contains__(self, item):
+        # type: (Union[ParsedVersion, str]) -> bool
         return self.contains(item)
 
     def contains(self, item, prereleases=None):
+        # type: (Union[ParsedVersion, str], Optional[bool]) -> bool
+
         # Ensure that our item is a Version or LegacyVersion instance.
         if not isinstance(item, (LegacyVersion, Version)):
             item = parse(item)
@@ -701,7 +809,13 @@ class SpecifierSet(BaseSpecifier):
         #       will always return True, this is an explicit design decision.
         return all(s.contains(item, prereleases=prereleases) for s in self._specs)
 
-    def filter(self, iterable, prereleases=None):
+    def filter(
+        self,
+        iterable,  # type: Iterable[Union[ParsedVersion, str]]
+        prereleases=None,  # type: Optional[bool]
+    ):
+        # type: (...) -> Iterable[Union[ParsedVersion, str]]
+
         # 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
         # SpecifierSet thinks for whether or not we should support prereleases.
@@ -719,8 +833,8 @@ class SpecifierSet(BaseSpecifier):
         # which will filter out any pre-releases, unless there are no final
         # releases, and which will filter out LegacyVersion in general.
         else:
-            filtered = []
-            found_prereleases = []
+            filtered = []  # type: List[Union[ParsedVersion, str]]
+            found_prereleases = []  # type: List[Union[ParsedVersion, str]]
 
             for item in iterable:
                 # Ensure that we some kind of Version class for this item.
index ec9942f0f6627f34554082a8c0909bc70bd2a260..9064910b8bafe2d60ce5fca8897226f5e0fb8f8f 100644 (file)
@@ -13,12 +13,37 @@ except ImportError:  # pragma: no cover
 
     EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()]
     del imp
+import logging
+import os
 import platform
 import re
+import struct
 import sys
 import sysconfig
 import warnings
 
+from ._typing import TYPE_CHECKING, cast
+
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import (
+        Dict,
+        FrozenSet,
+        IO,
+        Iterable,
+        Iterator,
+        List,
+        Optional,
+        Sequence,
+        Tuple,
+        Union,
+    )
+
+    PythonVersion = Sequence[int]
+    MacVersion = Tuple[int, int]
+    GlibcVersion = Tuple[int, int]
+
+
+logger = logging.getLogger(__name__)
 
 INTERPRETER_SHORT_NAMES = {
     "python": "py",  # Generic.
@@ -26,34 +51,48 @@ INTERPRETER_SHORT_NAMES = {
     "pypy": "pp",
     "ironpython": "ip",
     "jython": "jy",
-}
+}  # type: Dict[str, str]
 
 
 _32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32
 
 
 class Tag(object):
+    """
+    A representation of the tag triple for a wheel.
+
+    Instances are considered immutable and thus are hashable. Equality checking
+    is also supported.
+    """
 
     __slots__ = ["_interpreter", "_abi", "_platform"]
 
     def __init__(self, interpreter, abi, platform):
+        # type: (str, str, str) -> None
         self._interpreter = interpreter.lower()
         self._abi = abi.lower()
         self._platform = platform.lower()
 
     @property
     def interpreter(self):
+        # type: () -> str
         return self._interpreter
 
     @property
     def abi(self):
+        # type: () -> str
         return self._abi
 
     @property
     def platform(self):
+        # type: () -> str
         return self._platform
 
     def __eq__(self, other):
+        # type: (object) -> bool
+        if not isinstance(other, Tag):
+            return NotImplemented
+
         return (
             (self.platform == other.platform)
             and (self.abi == other.abi)
@@ -61,16 +100,26 @@ class Tag(object):
         )
 
     def __hash__(self):
+        # type: () -> int
         return hash((self._interpreter, self._abi, self._platform))
 
     def __str__(self):
+        # type: () -> str
         return "{}-{}-{}".format(self._interpreter, self._abi, self._platform)
 
     def __repr__(self):
+        # type: () -> str
         return "<{self} @ {self_id}>".format(self=self, self_id=id(self))
 
 
 def parse_tag(tag):
+    # type: (str) -> FrozenSet[Tag]
+    """
+    Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
+
+    Returning a set is required due to the possibility that the tag is a
+    compressed tag set.
+    """
     tags = set()
     interpreters, abis, platforms = tag.split("-")
     for interpreter in interpreters.split("."):
@@ -80,20 +129,54 @@ def parse_tag(tag):
     return frozenset(tags)
 
 
+def _warn_keyword_parameter(func_name, kwargs):
+    # type: (str, Dict[str, bool]) -> bool
+    """
+    Backwards-compatibility with Python 2.7 to allow treating 'warn' as keyword-only.
+    """
+    if not kwargs:
+        return False
+    elif len(kwargs) > 1 or "warn" not in kwargs:
+        kwargs.pop("warn", None)
+        arg = next(iter(kwargs.keys()))
+        raise TypeError(
+            "{}() got an unexpected keyword argument {!r}".format(func_name, arg)
+        )
+    return kwargs["warn"]
+
+
+def _get_config_var(name, warn=False):
+    # type: (str, bool) -> Union[int, str, None]
+    value = sysconfig.get_config_var(name)
+    if value is None and warn:
+        logger.debug(
+            "Config variable '%s' is unset, Python ABI tag may be incorrect", name
+        )
+    return value
+
+
 def _normalize_string(string):
+    # type: (str) -> str
     return string.replace(".", "_").replace("-", "_")
 
 
-def _cpython_interpreter(py_version):
-    # TODO: Is using py_version_nodot for interpreter version critical?
-    return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1])
+def _abi3_applies(python_version):
+    # type: (PythonVersion) -> bool
+    """
+    Determine if the Python version supports abi3.
+
+    PEP 384 was first implemented in Python 3.2.
+    """
+    return len(python_version) > 1 and tuple(python_version) >= (3, 2)
 
 
-def _cpython_abis(py_version):
+def _cpython_abis(py_version, warn=False):
+    # type: (PythonVersion, bool) -> List[str]
+    py_version = tuple(py_version)  # To allow for version comparison.
     abis = []
-    version = "{}{}".format(*py_version[:2])
+    version = _version_nodot(py_version[:2])
     debug = pymalloc = ucs4 = ""
-    with_debug = sysconfig.get_config_var("Py_DEBUG")
+    with_debug = _get_config_var("Py_DEBUG", warn)
     has_refcount = hasattr(sys, "gettotalrefcount")
     # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled
     # extension modules is the best option.
@@ -102,11 +185,11 @@ def _cpython_abis(py_version):
     if with_debug or (with_debug is None and (has_refcount or has_ext)):
         debug = "d"
     if py_version < (3, 8):
-        with_pymalloc = sysconfig.get_config_var("WITH_PYMALLOC")
+        with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
         if with_pymalloc or with_pymalloc is None:
             pymalloc = "m"
         if py_version < (3, 3):
-            unicode_size = sysconfig.get_config_var("Py_UNICODE_SIZE")
+            unicode_size = _get_config_var("Py_UNICODE_SIZE", warn)
             if unicode_size == 4 or (
                 unicode_size is None and sys.maxunicode == 0x10FFFF
             ):
@@ -124,86 +207,148 @@ def _cpython_abis(py_version):
     return abis
 
 
-def _cpython_tags(py_version, interpreter, abis, platforms):
+def cpython_tags(
+    python_version=None,  # type: Optional[PythonVersion]
+    abis=None,  # type: Optional[Iterable[str]]
+    platforms=None,  # type: Optional[Iterable[str]]
+    **kwargs  # type: bool
+):
+    # type: (...) -> Iterator[Tag]
+    """
+    Yields the tags for a CPython interpreter.
+
+    The tags consist of:
+    - cp<python_version>-<abi>-<platform>
+    - cp<python_version>-abi3-<platform>
+    - cp<python_version>-none-<platform>
+    - cp<less than python_version>-abi3-<platform>  # Older Python versions down to 3.2.
+
+    If python_version only specifies a major version then user-provided ABIs and
+    the 'none' ABItag will be used.
+
+    If 'abi3' or 'none' are specified in 'abis' then they will be yielded at
+    their normal position and not at the beginning.
+    """
+    warn = _warn_keyword_parameter("cpython_tags", kwargs)
+    if not python_version:
+        python_version = sys.version_info[:2]
+
+    interpreter = "cp{}".format(_version_nodot(python_version[:2]))
+
+    if abis is None:
+        if len(python_version) > 1:
+            abis = _cpython_abis(python_version, warn)
+        else:
+            abis = []
+    abis = list(abis)
+    # 'abi3' and 'none' are explicitly handled later.
+    for explicit_abi in ("abi3", "none"):
+        try:
+            abis.remove(explicit_abi)
+        except ValueError:
+            pass
+
+    platforms = list(platforms or _platform_tags())
     for abi in abis:
         for platform_ in platforms:
             yield Tag(interpreter, abi, platform_)
-    for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms):
-        yield tag
+    if _abi3_applies(python_version):
+        for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms):
+            yield tag
     for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms):
         yield tag
-    # PEP 384 was first implemented in Python 3.2.
-    for minor_version in range(py_version[1] - 1, 1, -1):
-        for platform_ in platforms:
-            interpreter = "cp{major}{minor}".format(
-                major=py_version[0], minor=minor_version
-            )
-            yield Tag(interpreter, "abi3", platform_)
-
 
-def _pypy_interpreter():
-    return "pp{py_major}{pypy_major}{pypy_minor}".format(
-        py_major=sys.version_info[0],
-        pypy_major=sys.pypy_version_info.major,
-        pypy_minor=sys.pypy_version_info.minor,
-    )
+    if _abi3_applies(python_version):
+        for minor_version in range(python_version[1] - 1, 1, -1):
+            for platform_ in platforms:
+                interpreter = "cp{version}".format(
+                    version=_version_nodot((python_version[0], minor_version))
+                )
+                yield Tag(interpreter, "abi3", platform_)
 
 
 def _generic_abi():
+    # type: () -> Iterator[str]
     abi = sysconfig.get_config_var("SOABI")
     if abi:
-        return _normalize_string(abi)
-    else:
-        return "none"
+        yield _normalize_string(abi)
 
 
-def _pypy_tags(py_version, interpreter, abi, platforms):
-    for tag in (Tag(interpreter, abi, platform) for platform in platforms):
-        yield tag
-    for tag in (Tag(interpreter, "none", platform) for platform in platforms):
-        yield tag
+def generic_tags(
+    interpreter=None,  # type: Optional[str]
+    abis=None,  # type: Optional[Iterable[str]]
+    platforms=None,  # type: Optional[Iterable[str]]
+    **kwargs  # type: bool
+):
+    # type: (...) -> Iterator[Tag]
+    """
+    Yields the tags for a generic interpreter.
 
+    The tags consist of:
+    - <interpreter>-<abi>-<platform>
 
-def _generic_tags(interpreter, py_version, abi, platforms):
-    for tag in (Tag(interpreter, abi, platform) for platform in platforms):
-        yield tag
-    if abi != "none":
-        tags = (Tag(interpreter, "none", platform_) for platform_ in platforms)
-        for tag in tags:
-            yield tag
+    The "none" ABI will be added if it was not explicitly provided.
+    """
+    warn = _warn_keyword_parameter("generic_tags", kwargs)
+    if not interpreter:
+        interp_name = interpreter_name()
+        interp_version = interpreter_version(warn=warn)
+        interpreter = "".join([interp_name, interp_version])
+    if abis is None:
+        abis = _generic_abi()
+    platforms = list(platforms or _platform_tags())
+    abis = list(abis)
+    if "none" not in abis:
+        abis.append("none")
+    for abi in abis:
+        for platform_ in platforms:
+            yield Tag(interpreter, abi, platform_)
 
 
 def _py_interpreter_range(py_version):
+    # type: (PythonVersion) -> Iterator[str]
     """
-    Yield Python versions in descending order.
+    Yields Python versions in descending order.
 
     After the latest version, the major-only version will be yielded, and then
-    all following versions up to 'end'.
+    all previous versions of that major version.
     """
-    yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1])
+    if len(py_version) > 1:
+        yield "py{version}".format(version=_version_nodot(py_version[:2]))
     yield "py{major}".format(major=py_version[0])
-    for minor in range(py_version[1] - 1, -1, -1):
-        yield "py{major}{minor}".format(major=py_version[0], minor=minor)
+    if len(py_version) > 1:
+        for minor in range(py_version[1] - 1, -1, -1):
+            yield "py{version}".format(version=_version_nodot((py_version[0], minor)))
 
 
-def _independent_tags(interpreter, py_version, platforms):
+def compatible_tags(
+    python_version=None,  # type: Optional[PythonVersion]
+    interpreter=None,  # type: Optional[str]
+    platforms=None,  # type: Optional[Iterable[str]]
+):
+    # type: (...) -> Iterator[Tag]
     """
-    Return the sequence of tags that are consistent across implementations.
+    Yields the sequence of tags that are compatible with a specific version of Python.
 
     The tags consist of:
     - py*-none-<platform>
-    - <interpreter>-none-any
+    - <interpreter>-none-any  # ... if `interpreter` is provided.
     - py*-none-any
     """
-    for version in _py_interpreter_range(py_version):
+    if not python_version:
+        python_version = sys.version_info[:2]
+    platforms = list(platforms or _platform_tags())
+    for version in _py_interpreter_range(python_version):
         for platform_ in platforms:
             yield Tag(version, "none", platform_)
-    yield Tag(interpreter, "none", "any")
-    for version in _py_interpreter_range(py_version):
+    if interpreter:
+        yield Tag(interpreter, "none", "any")
+    for version in _py_interpreter_range(python_version):
         yield Tag(version, "none", "any")
 
 
 def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER):
+    # type: (str, bool) -> str
     if not is_32bit:
         return arch
 
@@ -214,6 +359,7 @@ def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER):
 
 
 def _mac_binary_formats(version, cpu_arch):
+    # type: (MacVersion, str) -> List[str]
     formats = [cpu_arch]
     if cpu_arch == "x86_64":
         if version < (10, 4):
@@ -240,32 +386,42 @@ def _mac_binary_formats(version, cpu_arch):
     return formats
 
 
-def _mac_platforms(version=None, arch=None):
-    version_str, _, cpu_arch = platform.mac_ver()
+def mac_platforms(version=None, arch=None):
+    # type: (Optional[MacVersion], Optional[str]) -> Iterator[str]
+    """
+    Yields the platform tags for a macOS system.
+
+    The `version` parameter is a two-item tuple specifying the macOS version to
+    generate platform tags for. The `arch` parameter is the CPU architecture to
+    generate platform tags for. Both parameters default to the appropriate value
+    for the current system.
+    """
+    version_str, _, cpu_arch = platform.mac_ver()  # type: ignore
     if version is None:
-        version = tuple(map(int, version_str.split(".")[:2]))
+        version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
+    else:
+        version = version
     if arch is None:
         arch = _mac_arch(cpu_arch)
-    platforms = []
+    else:
+        arch = arch
     for minor_version in range(version[1], -1, -1):
         compat_version = version[0], minor_version
         binary_formats = _mac_binary_formats(compat_version, arch)
         for binary_format in binary_formats:
-            platforms.append(
-                "macosx_{major}_{minor}_{binary_format}".format(
-                    major=compat_version[0],
-                    minor=compat_version[1],
-                    binary_format=binary_format,
-                )
+            yield "macosx_{major}_{minor}_{binary_format}".format(
+                major=compat_version[0],
+                minor=compat_version[1],
+                binary_format=binary_format,
             )
-    return platforms
 
 
 # From PEP 513.
 def _is_manylinux_compatible(name, glibc_version):
+    # type: (str, GlibcVersion) -> bool
     # Check for presence of _manylinux module.
     try:
-        import _manylinux
+        import _manylinux  # noqa
 
         return bool(getattr(_manylinux, name + "_compatible"))
     except (ImportError, AttributeError):
@@ -276,14 +432,50 @@ def _is_manylinux_compatible(name, glibc_version):
 
 
 def _glibc_version_string():
+    # type: () -> Optional[str]
     # Returns glibc version string, or None if not using glibc.
-    import ctypes
+    return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
+
+
+def _glibc_version_string_confstr():
+    # type: () -> Optional[str]
+    """
+    Primary implementation of glibc_version_string using os.confstr.
+    """
+    # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
+    # to be broken or missing. This strategy is used in the standard library
+    # platform module.
+    # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183
+    try:
+        # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17".
+        version_string = os.confstr(  # type: ignore[attr-defined] # noqa: F821
+            "CS_GNU_LIBC_VERSION"
+        )
+        assert version_string is not None
+        _, version = version_string.split()  # type: Tuple[str, str]
+    except (AssertionError, AttributeError, OSError, ValueError):
+        # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
+        return None
+    return version
+
+
+def _glibc_version_string_ctypes():
+    # type: () -> Optional[str]
+    """
+    Fallback implementation of glibc_version_string using ctypes.
+    """
+    try:
+        import ctypes
+    except ImportError:
+        return None
 
     # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
     # manpage says, "If filename is NULL, then the returned handle is for the
     # main program". This way we can let the linker do the work to figure out
     # which libc our process is actually using.
-    process_namespace = ctypes.CDLL(None)
+    #
+    # Note: typeshed is wrong here so we are ignoring this line.
+    process_namespace = ctypes.CDLL(None)  # type: ignore
     try:
         gnu_get_libc_version = process_namespace.gnu_get_libc_version
     except AttributeError:
@@ -293,7 +485,7 @@ def _glibc_version_string():
 
     # Call gnu_get_libc_version, which returns a string like "2.5"
     gnu_get_libc_version.restype = ctypes.c_char_p
-    version_str = gnu_get_libc_version()
+    version_str = gnu_get_libc_version()  # type: str
     # py2 / py3 compatibility:
     if not isinstance(version_str, str):
         version_str = version_str.decode("ascii")
@@ -303,6 +495,7 @@ def _glibc_version_string():
 
 # Separated out from have_compatible_glibc for easier unit testing.
 def _check_glibc_version(version_str, required_major, minimum_minor):
+    # type: (str, int, int) -> bool
     # Parse string and check against requested version.
     #
     # We use a regexp instead of str.split because we want to discard any
@@ -324,81 +517,235 @@ def _check_glibc_version(version_str, required_major, minimum_minor):
 
 
 def _have_compatible_glibc(required_major, minimum_minor):
+    # type: (int, int) -> bool
     version_str = _glibc_version_string()
     if version_str is None:
         return False
     return _check_glibc_version(version_str, required_major, minimum_minor)
 
 
+# Python does not provide platform information at sufficient granularity to
+# identify the architecture of the running executable in some cases, so we
+# determine it dynamically by reading the information from the running
+# process. This only applies on Linux, which uses the ELF format.
+class _ELFFileHeader(object):
+    # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
+    class _InvalidELFFileHeader(ValueError):
+        """
+        An invalid ELF file header was found.
+        """
+
+    ELF_MAGIC_NUMBER = 0x7F454C46
+    ELFCLASS32 = 1
+    ELFCLASS64 = 2
+    ELFDATA2LSB = 1
+    ELFDATA2MSB = 2
+    EM_386 = 3
+    EM_S390 = 22
+    EM_ARM = 40
+    EM_X86_64 = 62
+    EF_ARM_ABIMASK = 0xFF000000
+    EF_ARM_ABI_VER5 = 0x05000000
+    EF_ARM_ABI_FLOAT_HARD = 0x00000400
+
+    def __init__(self, file):
+        # type: (IO[bytes]) -> None
+        def unpack(fmt):
+            # type: (str) -> int
+            try:
+                (result,) = struct.unpack(
+                    fmt, file.read(struct.calcsize(fmt))
+                )  # type: (int, )
+            except struct.error:
+                raise _ELFFileHeader._InvalidELFFileHeader()
+            return result
+
+        self.e_ident_magic = unpack(">I")
+        if self.e_ident_magic != self.ELF_MAGIC_NUMBER:
+            raise _ELFFileHeader._InvalidELFFileHeader()
+        self.e_ident_class = unpack("B")
+        if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}:
+            raise _ELFFileHeader._InvalidELFFileHeader()
+        self.e_ident_data = unpack("B")
+        if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}:
+            raise _ELFFileHeader._InvalidELFFileHeader()
+        self.e_ident_version = unpack("B")
+        self.e_ident_osabi = unpack("B")
+        self.e_ident_abiversion = unpack("B")
+        self.e_ident_pad = file.read(7)
+        format_h = "<H" if self.e_ident_data == self.ELFDATA2LSB else ">H"
+        format_i = "<I" if self.e_ident_data == self.ELFDATA2LSB else ">I"
+        format_q = "<Q" if self.e_ident_data == self.ELFDATA2LSB else ">Q"
+        format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q
+        self.e_type = unpack(format_h)
+        self.e_machine = unpack(format_h)
+        self.e_version = unpack(format_i)
+        self.e_entry = unpack(format_p)
+        self.e_phoff = unpack(format_p)
+        self.e_shoff = unpack(format_p)
+        self.e_flags = unpack(format_i)
+        self.e_ehsize = unpack(format_h)
+        self.e_phentsize = unpack(format_h)
+        self.e_phnum = unpack(format_h)
+        self.e_shentsize = unpack(format_h)
+        self.e_shnum = unpack(format_h)
+        self.e_shstrndx = unpack(format_h)
+
+
+def _get_elf_header():
+    # type: () -> Optional[_ELFFileHeader]
+    try:
+        with open(sys.executable, "rb") as f:
+            elf_header = _ELFFileHeader(f)
+    except (IOError, OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader):
+        return None
+    return elf_header
+
+
+def _is_linux_armhf():
+    # type: () -> bool
+    # hard-float ABI can be detected from the ELF header of the running
+    # process
+    # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
+    elf_header = _get_elf_header()
+    if elf_header is None:
+        return False
+    result = elf_header.e_ident_class == elf_header.ELFCLASS32
+    result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
+    result &= elf_header.e_machine == elf_header.EM_ARM
+    result &= (
+        elf_header.e_flags & elf_header.EF_ARM_ABIMASK
+    ) == elf_header.EF_ARM_ABI_VER5
+    result &= (
+        elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD
+    ) == elf_header.EF_ARM_ABI_FLOAT_HARD
+    return result
+
+
+def _is_linux_i686():
+    # type: () -> bool
+    elf_header = _get_elf_header()
+    if elf_header is None:
+        return False
+    result = elf_header.e_ident_class == elf_header.ELFCLASS32
+    result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
+    result &= elf_header.e_machine == elf_header.EM_386
+    return result
+
+
+def _have_compatible_manylinux_abi(arch):
+    # type: (str) -> bool
+    if arch == "armv7l":
+        return _is_linux_armhf()
+    if arch == "i686":
+        return _is_linux_i686()
+    return True
+
+
 def _linux_platforms(is_32bit=_32_BIT_INTERPRETER):
+    # type: (bool) -> Iterator[str]
     linux = _normalize_string(distutils.util.get_platform())
-    if linux == "linux_x86_64" and is_32bit:
-        linux = "linux_i686"
-    manylinux_support = (
-        ("manylinux2014", (2, 17)),  # CentOS 7 w/ glibc 2.17 (PEP 599)
-        ("manylinux2010", (2, 12)),  # CentOS 6 w/ glibc 2.12 (PEP 571)
-        ("manylinux1", (2, 5)),  # CentOS 5 w/ glibc 2.5 (PEP 513)
-    )
+    if is_32bit:
+        if linux == "linux_x86_64":
+            linux = "linux_i686"
+        elif linux == "linux_aarch64":
+            linux = "linux_armv7l"
+    manylinux_support = []
+    _, arch = linux.split("_", 1)
+    if _have_compatible_manylinux_abi(arch):
+        if arch in {"x86_64", "i686", "aarch64", "armv7l", "ppc64", "ppc64le", "s390x"}:
+            manylinux_support.append(
+                ("manylinux2014", (2, 17))
+            )  # CentOS 7 w/ glibc 2.17 (PEP 599)
+        if arch in {"x86_64", "i686"}:
+            manylinux_support.append(
+                ("manylinux2010", (2, 12))
+            )  # CentOS 6 w/ glibc 2.12 (PEP 571)
+            manylinux_support.append(
+                ("manylinux1", (2, 5))
+            )  # CentOS 5 w/ glibc 2.5 (PEP 513)
     manylinux_support_iter = iter(manylinux_support)
     for name, glibc_version in manylinux_support_iter:
         if _is_manylinux_compatible(name, glibc_version):
-            platforms = [linux.replace("linux", name)]
+            yield linux.replace("linux", name)
             break
-    else:
-        platforms = []
     # Support for a later manylinux implies support for an earlier version.
-    platforms += [linux.replace("linux", name) for name, _ in manylinux_support_iter]
-    platforms.append(linux)
-    return platforms
+    for name, _ in manylinux_support_iter:
+        yield linux.replace("linux", name)
+    yield linux
 
 
 def _generic_platforms():
-    platform = _normalize_string(distutils.util.get_platform())
-    return [platform]
+    # type: () -> Iterator[str]
+    yield _normalize_string(distutils.util.get_platform())
 
 
-def _interpreter_name():
-    name = platform.python_implementation().lower()
+def _platform_tags():
+    # type: () -> Iterator[str]
+    """
+    Provides the platform tags for this installation.
+    """
+    if platform.system() == "Darwin":
+        return mac_platforms()
+    elif platform.system() == "Linux":
+        return _linux_platforms()
+    else:
+        return _generic_platforms()
+
+
+def interpreter_name():
+    # type: () -> str
+    """
+    Returns the name of the running interpreter.
+    """
+    try:
+        name = sys.implementation.name  # type: ignore
+    except AttributeError:  # pragma: no cover
+        # Python 2.7 compatibility.
+        name = platform.python_implementation().lower()
     return INTERPRETER_SHORT_NAMES.get(name) or name
 
 
-def _generic_interpreter(name, py_version):
-    version = sysconfig.get_config_var("py_version_nodot")
-    if not version:
-        version = "".join(map(str, py_version[:2]))
-    return "{name}{version}".format(name=name, version=version)
+def interpreter_version(**kwargs):
+    # type: (bool) -> str
+    """
+    Returns the version of the running interpreter.
+    """
+    warn = _warn_keyword_parameter("interpreter_version", kwargs)
+    version = _get_config_var("py_version_nodot", warn=warn)
+    if version:
+        version = str(version)
+    else:
+        version = _version_nodot(sys.version_info[:2])
+    return version
+
+
+def _version_nodot(version):
+    # type: (PythonVersion) -> str
+    if any(v >= 10 for v in version):
+        sep = "_"
+    else:
+        sep = ""
+    return sep.join(map(str, version))
 
 
-def sys_tags():
+def sys_tags(**kwargs):
+    # type: (bool) -> Iterator[Tag]
     """
     Returns the sequence of tag triples for the running interpreter.
 
     The order of the sequence corresponds to priority order for the
     interpreter, from most to least important.
     """
-    py_version = sys.version_info[:2]
-    interpreter_name = _interpreter_name()
-    if platform.system() == "Darwin":
-        platforms = _mac_platforms()
-    elif platform.system() == "Linux":
-        platforms = _linux_platforms()
-    else:
-        platforms = _generic_platforms()
+    warn = _warn_keyword_parameter("sys_tags", kwargs)
 
-    if interpreter_name == "cp":
-        interpreter = _cpython_interpreter(py_version)
-        abis = _cpython_abis(py_version)
-        for tag in _cpython_tags(py_version, interpreter, abis, platforms):
-            yield tag
-    elif interpreter_name == "pp":
-        interpreter = _pypy_interpreter()
-        abi = _generic_abi()
-        for tag in _pypy_tags(py_version, interpreter, abi, platforms):
+    interp_name = interpreter_name()
+    if interp_name == "cp":
+        for tag in cpython_tags(warn=warn):
             yield tag
     else:
-        interpreter = _generic_interpreter(interpreter_name, py_version)
-        abi = _generic_abi()
-        for tag in _generic_tags(interpreter, py_version, abi, platforms):
+        for tag in generic_tags():
             yield tag
-    for tag in _independent_tags(interpreter, py_version, platforms):
+
+    for tag in compatible_tags():
         yield tag
index 88418786933b8bc5f6179b8e191f60f79efd7074..19579c1a0fa38c088a7cbb80950d0c85f5514cca 100644 (file)
@@ -5,28 +5,36 @@ from __future__ import absolute_import, division, print_function
 
 import re
 
+from ._typing import TYPE_CHECKING, cast
 from .version import InvalidVersion, Version
 
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import NewType, Union
+
+    NormalizedName = NewType("NormalizedName", str)
 
 _canonicalize_regex = re.compile(r"[-_.]+")
 
 
 def canonicalize_name(name):
+    # type: (str) -> NormalizedName
     # This is taken from PEP 503.
-    return _canonicalize_regex.sub("-", name).lower()
+    value = _canonicalize_regex.sub("-", name).lower()
+    return cast("NormalizedName", value)
 
 
-def canonicalize_version(version):
+def canonicalize_version(_version):
+    # type: (str) -> Union[Version, str]
     """
-    This is very similar to Version.__str__, but has one subtle differences
+    This is very similar to Version.__str__, but has one subtle difference
     with the way it handles the release segment.
     """
 
     try:
-        version = Version(version)
+        version = Version(_version)
     except InvalidVersion:
         # Legacy versions cannot be normalized
-        return version
+        return _version
 
     parts = []
 
index 95157a1f78c26829ffbe1bd2463f7735b636d16f..00371e86a87edfc5f8d1d1352360bfae0cce8e65 100644 (file)
@@ -7,8 +7,35 @@ import collections
 import itertools
 import re
 
-from ._structures import Infinity
-
+from ._structures import Infinity, NegativeInfinity
+from ._typing import TYPE_CHECKING
+
+if TYPE_CHECKING:  # pragma: no cover
+    from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union
+
+    from ._structures import InfinityType, NegativeInfinityType
+
+    InfiniteTypes = Union[InfinityType, NegativeInfinityType]
+    PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
+    SubLocalType = Union[InfiniteTypes, int, str]
+    LocalType = Union[
+        NegativeInfinityType,
+        Tuple[
+            Union[
+                SubLocalType,
+                Tuple[SubLocalType, str],
+                Tuple[NegativeInfinityType, SubLocalType],
+            ],
+            ...,
+        ],
+    ]
+    CmpKey = Tuple[
+        int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
+    ]
+    LegacyCmpKey = Tuple[int, Tuple[str, ...]]
+    VersionComparisonMethod = Callable[
+        [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool
+    ]
 
 __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
 
@@ -19,6 +46,7 @@ _Version = collections.namedtuple(
 
 
 def parse(version):
+    # type: (str) -> Union[LegacyVersion, 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
@@ -37,28 +65,38 @@ class InvalidVersion(ValueError):
 
 
 class _BaseVersion(object):
+    _key = None  # type: Union[CmpKey, LegacyCmpKey]
+
     def __hash__(self):
+        # type: () -> int
         return hash(self._key)
 
     def __lt__(self, other):
+        # type: (_BaseVersion) -> bool
         return self._compare(other, lambda s, o: s < o)
 
     def __le__(self, other):
+        # type: (_BaseVersion) -> bool
         return self._compare(other, lambda s, o: s <= o)
 
     def __eq__(self, other):
+        # type: (object) -> bool
         return self._compare(other, lambda s, o: s == o)
 
     def __ge__(self, other):
+        # type: (_BaseVersion) -> bool
         return self._compare(other, lambda s, o: s >= o)
 
     def __gt__(self, other):
+        # type: (_BaseVersion) -> bool
         return self._compare(other, lambda s, o: s > o)
 
     def __ne__(self, other):
+        # type: (object) -> bool
         return self._compare(other, lambda s, o: s != o)
 
     def _compare(self, other, method):
+        # type: (object, VersionComparisonMethod) -> Union[bool, NotImplemented]
         if not isinstance(other, _BaseVersion):
             return NotImplemented
 
@@ -67,57 +105,71 @@ class _BaseVersion(object):
 
 class LegacyVersion(_BaseVersion):
     def __init__(self, version):
+        # type: (str) -> None
         self._version = str(version)
         self._key = _legacy_cmpkey(self._version)
 
     def __str__(self):
+        # type: () -> str
         return self._version
 
     def __repr__(self):
+        # type: () -> str
         return "<LegacyVersion({0})>".format(repr(str(self)))
 
     @property
     def public(self):
+        # type: () -> str
         return self._version
 
     @property
     def base_version(self):
+        # type: () -> str
         return self._version
 
     @property
     def epoch(self):
+        # type: () -> int
         return -1
 
     @property
     def release(self):
+        # type: () -> None
         return None
 
     @property
     def pre(self):
+        # type: () -> None
         return None
 
     @property
     def post(self):
+        # type: () -> None
         return None
 
     @property
     def dev(self):
+        # type: () -> None
         return None
 
     @property
     def local(self):
+        # type: () -> None
         return None
 
     @property
     def is_prerelease(self):
+        # type: () -> bool
         return False
 
     @property
     def is_postrelease(self):
+        # type: () -> bool
         return False
 
     @property
     def is_devrelease(self):
+        # type: () -> bool
         return False
 
 
@@ -133,6 +185,7 @@ _legacy_version_replacement_map = {
 
 
 def _parse_version_parts(s):
+    # type: (str) -> Iterator[str]
     for part in _legacy_version_component_re.split(s):
         part = _legacy_version_replacement_map.get(part, part)
 
@@ -150,6 +203,8 @@ def _parse_version_parts(s):
 
 
 def _legacy_cmpkey(version):
+    # type: (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,
@@ -158,7 +213,7 @@ def _legacy_cmpkey(version):
 
     # This scheme is taken from pkg_resources.parse_version setuptools prior to
     # it's adoption of the packaging library.
-    parts = []
+    parts = []  # type: List[str]
     for part in _parse_version_parts(version.lower()):
         if part.startswith("*"):
             # remove "-" before a prerelease tag
@@ -171,9 +226,8 @@ def _legacy_cmpkey(version):
                 parts.pop()
 
         parts.append(part)
-    parts = tuple(parts)
 
-    return epoch, parts
+    return epoch, tuple(parts)
 
 
 # Deliberately not anchored to the start and end of the string, to make it
@@ -215,6 +269,8 @@ class Version(_BaseVersion):
     _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
 
     def __init__(self, version):
+        # type: (str) -> None
+
         # Validate the version and parse it into pieces
         match = self._regex.search(version)
         if not match:
@@ -243,9 +299,11 @@ class Version(_BaseVersion):
         )
 
     def __repr__(self):
+        # type: () -> str
         return "<Version({0})>".format(repr(str(self)))
 
     def __str__(self):
+        # type: () -> str
         parts = []
 
         # Epoch
@@ -275,26 +333,35 @@ class Version(_BaseVersion):
 
     @property
     def epoch(self):
-        return self._version.epoch
+        # type: () -> int
+        _epoch = self._version.epoch  # type: int
+        return _epoch
 
     @property
     def release(self):
-        return self._version.release
+        # type: () -> Tuple[int, ...]
+        _release = self._version.release  # type: Tuple[int, ...]
+        return _release
 
     @property
     def pre(self):
-        return self._version.pre
+        # type: () -> Optional[Tuple[str, int]]
+        _pre = self._version.pre  # type: Optional[Tuple[str, int]]
+        return _pre
 
     @property
     def post(self):
+        # type: () -> Optional[Tuple[str, int]]
         return self._version.post[1] if self._version.post else None
 
     @property
     def dev(self):
+        # type: () -> Optional[Tuple[str, int]]
         return self._version.dev[1] if self._version.dev else None
 
     @property
     def local(self):
+        # type: () -> Optional[str]
         if self._version.local:
             return ".".join(str(x) for x in self._version.local)
         else:
@@ -302,10 +369,12 @@ class Version(_BaseVersion):
 
     @property
     def public(self):
+        # type: () -> str
         return str(self).split("+", 1)[0]
 
     @property
     def base_version(self):
+        # type: () -> str
         parts = []
 
         # Epoch
@@ -319,18 +388,41 @@ class Version(_BaseVersion):
 
     @property
     def is_prerelease(self):
+        # type: () -> bool
         return self.dev is not None or self.pre is not None
 
     @property
     def is_postrelease(self):
+        # type: () -> bool
         return self.post is not None
 
     @property
     def is_devrelease(self):
+        # type: () -> bool
         return self.dev is not None
 
+    @property
+    def major(self):
+        # type: () -> int
+        return self.release[0] if len(self.release) >= 1 else 0
+
+    @property
+    def minor(self):
+        # type: () -> int
+        return self.release[1] if len(self.release) >= 2 else 0
+
+    @property
+    def micro(self):
+        # type: () -> int
+        return self.release[2] if len(self.release) >= 3 else 0
+
+
+def _parse_letter_version(
+    letter,  # type: str
+    number,  # type: Union[str, bytes, SupportsInt]
+):
+    # type: (...) -> Optional[Tuple[str, int]]
 
-def _parse_letter_version(letter, number):
     if letter:
         # We consider there to be an implicit 0 in a pre-release if there is
         # not a numeral associated with it.
@@ -360,11 +452,14 @@ def _parse_letter_version(letter, number):
 
         return letter, int(number)
 
+    return None
+
 
 _local_version_separators = re.compile(r"[\._-]")
 
 
 def _parse_local_version(local):
+    # type: (str) -> Optional[LocalType]
     """
     Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
     """
@@ -373,15 +468,25 @@ def _parse_local_version(local):
             part.lower() if not part.isdigit() else int(part)
             for part in _local_version_separators.split(local)
         )
+    return None
+
 
+def _cmpkey(
+    epoch,  # type: int
+    release,  # type: Tuple[int, ...]
+    pre,  # type: Optional[Tuple[str, int]]
+    post,  # type: Optional[Tuple[str, int]]
+    dev,  # type: Optional[Tuple[str, int]]
+    local,  # type: Optional[Tuple[SubLocalType]]
+):
+    # type: (...) -> CmpKey
 
-def _cmpkey(epoch, release, pre, post, dev, local):
     # When we compare a release version, we want to compare it with all of the
     # trailing zeros removed. So we'll use a reverse the list, drop all the now
     # leading zeros until we come to something non zero, then take the rest
     # re-reverse it back into the correct order and make it a tuple and use
     # that for our sorting key.
-    release = tuple(
+    _release = tuple(
         reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
     )
 
@@ -390,23 +495,31 @@ def _cmpkey(epoch, release, pre, post, dev, local):
     # if there is not a pre or a post segment. If we have one of those then
     # the normal sorting rules will handle this case correctly.
     if pre is None and post is None and dev is not None:
-        pre = -Infinity
+        _pre = NegativeInfinity  # type: PrePostDevType
     # Versions without a pre-release (except as noted above) should sort after
     # those with one.
     elif pre is None:
-        pre = Infinity
+        _pre = Infinity
+    else:
+        _pre = pre
 
     # Versions without a post segment should sort before those with one.
     if post is None:
-        post = -Infinity
+        _post = NegativeInfinity  # type: PrePostDevType
+
+    else:
+        _post = post
 
     # Versions without a development segment should sort after those with one.
     if dev is None:
-        dev = Infinity
+        _dev = Infinity  # type: PrePostDevType
+
+    else:
+        _dev = dev
 
     if local is None:
         # Versions without a local segment should sort before those with one.
-        local = -Infinity
+        _local = NegativeInfinity  # type: LocalType
     else:
         # Versions with a local segment need that segment parsed to implement
         # the sorting rules in PEP440.
@@ -415,6 +528,8 @@ def _cmpkey(epoch, release, pre, post, dev, local):
         # - Numeric segments sort numerically
         # - Shorter versions sort before longer versions when the prefixes
         #   match exactly
-        local = tuple((i, "") if isinstance(i, int) else (-Infinity, i) for i in local)
+        _local = tuple(
+            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
+        )
 
-    return epoch, release, pre, post, dev, local
+    return epoch, _release, _pre, _post, _dev, _local
index 65183d9a2a9b22bcade6ac0ed28ad2b88405f907..288d077096e52e43dee42b2f36b58d21495d5852 100644 (file)
@@ -1,4 +1,4 @@
-packaging==19.2
+packaging==20.4
 pyparsing==2.2.1
 six==1.10.0
 ordered-set==3.1.1