From 33c13ee30541ac46478d5cf40a55ff058d614870 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 10 Sep 2019 18:46:24 -0400 Subject: [PATCH 001/114] Correct trailing comments. --- packaging/markers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/markers.py b/packaging/markers.py index 3b8af3242..8c8701261 100644 --- a/packaging/markers.py +++ b/packaging/markers.py @@ -85,13 +85,13 @@ def serialize(self): | 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") ) ALIASES = { "os.name": "os_name", From d50c55fb50b0abd63ffb481c404742a7b79a8b33 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 10 Sep 2019 18:47:39 -0400 Subject: [PATCH 002/114] Add reference for 'extra' marker variable. --- packaging/markers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/markers.py b/packaging/markers.py index 8c8701261..708dd9a28 100644 --- a/packaging/markers.py +++ b/packaging/markers.py @@ -91,7 +91,7 @@ def serialize(self): | L("platform.machine") # PEP-345 | L("platform.python_implementation") # PEP-345 | L("python_implementation") # undocumented setuptools legacy - | L("extra") + | L("extra") # PEP-508 ) ALIASES = { "os.name": "os_name", From 7c18feeefd2df2efab1dd0757f09ade8167463cd Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 18 Sep 2019 15:43:56 -0700 Subject: [PATCH 003/114] Bump version number for development --- CHANGELOG.rst | 5 +++++ packaging/__about__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 24a97655e..a42eb0a99 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog --------- +20.0 - master_ +~~~~~~~~~~~~~~~~~ + +.. note:: This version is not yet released and is under active development. + 19.2 - 2019-09-18 ~~~~~~~~~~~~~~~~~ diff --git a/packaging/__about__.py b/packaging/__about__.py index dc95138d0..565565f81 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "19.2" +__version__ = "20.0.dev0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From 72f6302131604f0fe8cdfcaaac6f89c2377ce7eb Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Sat, 14 Sep 2019 16:33:50 +0200 Subject: [PATCH 004/114] Define undefined behavior for Tag.__eq__ --- packaging/tags.py | 3 +++ tests/test_tags.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/packaging/tags.py b/packaging/tags.py index ec9942f0f..5058ac733 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -54,6 +54,9 @@ def platform(self): return self._platform def __eq__(self, other): + if not isinstance(other, Tag): + return NotImplemented + return ( (self.platform == other.platform) and (self.abi == other.abi) diff --git a/tests/test_tags.py b/tests/test_tags.py index 39927e684..4ad35478c 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -49,6 +49,10 @@ def test_tag_equality(): assert tags.Tag(*args) == tags.Tag(*args) +def test_tag_equality_fails_with_non_tag(): + assert not tags.Tag("py3", "none", "any") == "non-tag" + + def test_tag_hashing(example_tag): tags = {example_tag} # Should not raise TypeError. assert example_tag in tags From 2f8a31c0fb5a3e566592678a1281f8ff66217af5 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Sat, 14 Sep 2019 16:35:22 +0200 Subject: [PATCH 005/114] Configure a static typechecker --- .travis.yml | 1 + packaging/_typing.py | 29 +++++++++++++++++++++++++++++ setup.cfg | 4 ++++ tox.ini | 8 ++++++++ 4 files changed, 42 insertions(+) create mode 100644 packaging/_typing.py diff --git a/.travis.yml b/.travis.yml index c7ab82239..430e77dcb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ matrix: - python: 3.8-dev env: TOXENV=py dist: xenial + - env: TOXENV=mypy - env: TOXENV=lint - env: TOXENV=docs - env: TOXENV=packaging diff --git a/packaging/_typing.py b/packaging/_typing.py new file mode 100644 index 000000000..576770e6c --- /dev/null +++ b/packaging/_typing.py @@ -0,0 +1,29 @@ +"""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 MYPY_CHECK_RUNNING + + if MYPY_CHECK_RUNNING: + from typing import ... + +Ref: https://github.com/python/mypy/issues/3216 +""" + +MYPY_CHECK_RUNNING = False diff --git a/setup.cfg b/setup.cfg index 3c6e79cf3..6cfa4257e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [bdist_wheel] universal=1 + +[mypy] +ignore_missing_imports = True +mypy_path = stubs diff --git a/tox.ini b/tox.ini index e5fc0d4c9..3de45e221 100644 --- a/tox.ini +++ b/tox.ini @@ -29,6 +29,14 @@ commands = sphinx-build -W -b latex -d {envtmpdir}/doctrees docs docs/_build/latex sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html +[testenv:mypy] +basepython = python3 +deps = + mypy +commands = + mypy packaging --strict + mypy packaging --py2 --strict + [testenv:lint] basepython = python3 deps = From 8fb32c75a6a01d633dde87a5c5913b5be5f75622 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Sat, 14 Sep 2019 16:45:46 +0200 Subject: [PATCH 006/114] Some refactoring to ease typing --- packaging/_structures.py | 8 +++--- packaging/markers.py | 8 ++++-- packaging/specifiers.py | 56 ++++++++++++++++++++++------------------ packaging/tags.py | 14 ++++++---- packaging/utils.py | 6 ++--- packaging/version.py | 47 +++++++++++++++++++++++---------- 6 files changed, 86 insertions(+), 53 deletions(-) diff --git a/packaging/_structures.py b/packaging/_structures.py index 68dcca634..3c167756b 100644 --- a/packaging/_structures.py +++ b/packaging/_structures.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, division, print_function -class Infinity(object): +class InfinityType(object): def __repr__(self): return "Infinity" @@ -33,10 +33,10 @@ def __neg__(self): return NegativeInfinity -Infinity = Infinity() +Infinity = InfinityType() -class NegativeInfinity(object): +class NegativeInfinityType(object): def __repr__(self): return "-Infinity" @@ -65,4 +65,4 @@ def __neg__(self): return Infinity -NegativeInfinity = NegativeInfinity() +NegativeInfinity = NegativeInfinityType() diff --git a/packaging/markers.py b/packaging/markers.py index 3b8af3242..d8cc64ff4 100644 --- a/packaging/markers.py +++ b/packaging/markers.py @@ -192,13 +192,17 @@ 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) - if value is _undefined: + if isinstance(value, Undefined): raise UndefinedEnvironmentName( "{0!r} does not exist in evaluation environment.".format(name) ) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index 743576a08..d465a918b 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -106,7 +106,7 @@ def __hash__(self): def __eq__(self, other): 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__): @@ -117,7 +117,7 @@ def __eq__(self, other): def __ne__(self, other): 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,7 +126,10 @@ def __ne__(self, other): return self._spec != other._spec def _get_operator(self, op): - return getattr(self, "_compare_{0}".format(self._operators[op])) + operator_callable = getattr( + self, "_compare_{0}".format(self._operators[op]) + ) + return operator_callable def _coerce_version(self, version): if not isinstance(version, (LegacyVersion, Version)): @@ -159,17 +162,20 @@ def contains(self, item, prereleases=None): # 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 + ) + return operator_callable(normalized_item, self.version) def filter(self, iterable, prereleases=None): yielded = False @@ -406,32 +412,36 @@ def _compare_equal(self, prospective, spec): 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): @@ -446,10 +456,10 @@ def _compare_greater_than_equal(self, prospective, spec): return prospective >= Version(spec) @_require_version_compare - def _compare_less_than(self, prospective, spec): + def _compare_less_than(self, prospective, spec_str): # 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 +481,10 @@ def _compare_less_than(self, prospective, spec): return True @_require_version_compare - def _compare_greater_than(self, prospective, spec): + def _compare_greater_than(self, prospective, spec_str): # 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 @@ -569,12 +579,12 @@ class SpecifierSet(BaseSpecifier): def __init__(self, specifiers="", prereleases=None): # Split on , to break each indidivual 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: @@ -626,9 +636,7 @@ def __and__(self, other): return specifier def __eq__(self, other): - if isinstance(other, string_types): - other = SpecifierSet(other) - elif isinstance(other, _IndividualSpecifier): + if isinstance(other, (string_types, _IndividualSpecifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): return NotImplemented @@ -636,9 +644,7 @@ def __eq__(self, other): return self._specs == other._specs def __ne__(self, other): - if isinstance(other, string_types): - other = SpecifierSet(other) - elif isinstance(other, _IndividualSpecifier): + if isinstance(other, (string_types, _IndividualSpecifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): return NotImplemented diff --git a/packaging/tags.py b/packaging/tags.py index 5058ac733..6c8a7962d 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -246,13 +246,17 @@ def _mac_binary_formats(version, cpu_arch): def _mac_platforms(version=None, arch=None): version_str, _, cpu_arch = platform.mac_ver() if version is None: - version = tuple(map(int, version_str.split(".")[:2])) + _version = tuple(map(int, version_str.split(".")[:2])) + else: + _version = version if arch is None: - arch = _mac_arch(cpu_arch) + _arch = _mac_arch(cpu_arch) + else: + _arch = arch platforms = [] - for minor_version in range(version[1], -1, -1): - compat_version = version[0], minor_version - binary_formats = _mac_binary_formats(compat_version, 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( diff --git a/packaging/utils.py b/packaging/utils.py index 884187869..6b97824f2 100644 --- a/packaging/utils.py +++ b/packaging/utils.py @@ -16,17 +16,17 @@ def canonicalize_name(name): return _canonicalize_regex.sub("-", name).lower() -def canonicalize_version(version): +def canonicalize_version(_version): """ This is very similar to Version.__str__, but has one subtle differences 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 = [] diff --git a/packaging/version.py b/packaging/version.py index 95157a1f7..8c9728ec5 100644 --- a/packaging/version.py +++ b/packaging/version.py @@ -7,7 +7,7 @@ import itertools import re -from ._structures import Infinity +from ._structures import Infinity, NegativeInfinity __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] @@ -37,6 +37,8 @@ class InvalidVersion(ValueError): class _BaseVersion(object): + _key = None + def __hash__(self): return hash(self._key) @@ -171,9 +173,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 @@ -275,15 +276,18 @@ def __str__(self): @property def epoch(self): - return self._version.epoch + _epoch = self._version.epoch + return _epoch @property def release(self): - return self._version.release + _release = self._version.release + return _release @property def pre(self): - return self._version.pre + _pre = self._version.pre + return _pre @property def post(self): @@ -360,6 +364,8 @@ def _parse_letter_version(letter, number): return letter, int(number) + return None + _local_version_separators = re.compile(r"[\._-]") @@ -373,6 +379,8 @@ 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, release, pre, post, dev, local): @@ -381,7 +389,7 @@ def _cmpkey(epoch, release, pre, post, dev, local): # 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 +398,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 # 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 + + else: + _post = post # Versions without a development segment should sort after those with one. if dev is None: - dev = Infinity + _dev = Infinity + + else: + _dev = dev if local is None: # Versions without a local segment should sort before those with one. - local = -Infinity + _local = NegativeInfinity else: # Versions with a local segment need that segment parsed to implement # the sorting rules in PEP440. @@ -415,6 +431,9 @@ 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 From 7c23e49f58562a6a047d6efd494e9d13684e88cc Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Sat, 14 Sep 2019 20:03:21 +0200 Subject: [PATCH 007/114] Add type annotations --- MANIFEST.in | 1 + packaging/_compat.py | 9 ++- packaging/_structures.py | 18 ++++++ packaging/markers.py | 33 +++++++++-- packaging/requirements.py | 9 ++- packaging/specifiers.py | 115 ++++++++++++++++++++++++++++++++++---- packaging/tags.py | 48 ++++++++++++++-- packaging/utils.py | 5 ++ packaging/version.py | 109 +++++++++++++++++++++++++++++++----- stubs/platform.pyi | 8 +++ 10 files changed, 321 insertions(+), 34 deletions(-) create mode 100644 stubs/platform.pyi diff --git a/MANIFEST.in b/MANIFEST.in index 36fcbd8e6..c9804a4be 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,7 @@ include tox.ini recursive-include docs * recursive-include tests *.py +recursive-include stubs * exclude .travis.yml exclude dev-requirements.txt diff --git a/packaging/_compat.py b/packaging/_compat.py index 25da473c1..a145f7eeb 100644 --- a/packaging/_compat.py +++ b/packaging/_compat.py @@ -5,6 +5,11 @@ import sys +from ._typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: # 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 @@ 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", (), {}) diff --git a/packaging/_structures.py b/packaging/_structures.py index 3c167756b..800d5c558 100644 --- a/packaging/_structures.py +++ b/packaging/_structures.py @@ -6,30 +6,39 @@ 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 @@ -38,30 +47,39 @@ def __neg__(self): 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 diff --git a/packaging/markers.py b/packaging/markers.py index d8cc64ff4..45b7e2a38 100644 --- a/packaging/markers.py +++ b/packaging/markers.py @@ -13,8 +13,14 @@ from pyparsing import Literal as L # noqa from ._compat import string_types +from ._typing import MYPY_CHECK_RUNNING from .specifiers import Specifier, InvalidSpecifier +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import Any, List, Tuple, Dict, Union, Optional, Callable + + 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) @@ -131,6 +144,7 @@ def serialize(self): 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 @@ def _format_marker(marker, first=True): "!=": 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) @@ -200,7 +217,8 @@ class Undefined(object): 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 isinstance(value, Undefined): raise UndefinedEnvironmentName( @@ -211,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)) @@ -238,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": @@ -246,6 +266,7 @@ 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 @@ -270,6 +291,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: @@ -279,12 +301,15 @@ def __init__(self, marker): raise InvalidMarker(err_str) def __str__(self): + # type: () -> str return _format_marker(self._markers) def __repr__(self): + # type: () -> str return "".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/packaging/requirements.py b/packaging/requirements.py index 4d9688b93..1b547927d 100644 --- a/packaging/requirements.py +++ b/packaging/requirements.py @@ -11,9 +11,13 @@ from pyparsing import Literal as L # noqa from six.moves.urllib import parse as urlparse +from ._typing import MYPY_CHECK_RUNNING from .markers import MARKER_EXPR, Marker from .specifiers import LegacySpecifier, Specifier, SpecifierSet +if MYPY_CHECK_RUNNING: # 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 @@ def __init__(self, requirement_string): 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 @@ def __str__(self): return "".join(parts) def __repr__(self): + # type: () -> str return "".format(str(self)) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index d465a918b..091e9de7a 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -9,8 +9,25 @@ import re from ._compat import string_types, with_metaclass +from ._typing import MYPY_CHECK_RUNNING from .version import Version, LegacyVersion, parse +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import ( + List, + Dict, + Union, + Iterable, + Iterator, + Optional, + Callable, + Tuple, + FrozenSet, + ) + + ParsedVersion = Union[Version, LegacyVersion] + UnparsedVersion = Union[Version, LegacyVersion, str] + class InvalidSpecifier(ValueError): """ @@ -18,9 +35,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 +46,14 @@ def __str__(self): @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 +61,7 @@ def __eq__(self, other): @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 +69,7 @@ def __ne__(self, other): @abc.abstractproperty def prereleases(self): + # type: () -> Optional[bool] """ Returns whether or not pre-releases as a whole are allowed by this specifier. @@ -55,6 +77,7 @@ def prereleases(self): @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 +85,14 @@ def prereleases(self, value): @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 +101,24 @@ def filter(self, iterable, prereleases=None): 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,12 +128,15 @@ def __repr__(self): return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre) def __str__(self): + # type: () -> str return "{0}{1}".format(*self._spec) def __hash__(self): + # type: () -> int return hash(self._spec) def __eq__(self, other): + # type: (object) -> bool if isinstance(other, string_types): try: other = self.__class__(str(other)) @@ -115,6 +148,7 @@ def __eq__(self, other): return self._spec == other._spec def __ne__(self, other): + # type: (object) -> bool if isinstance(other, string_types): try: other = self.__class__(str(other)) @@ -126,36 +160,45 @@ def __ne__(self, other): return self._spec != other._spec def _get_operator(self, op): + # type: (str) -> Callable[[ParsedVersion, str], bool] operator_callable = getattr( self, "_compare_{0}".format(self._operators[op]) - ) + ) # type: Callable[[ParsedVersion, str], bool] 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 @@ -174,10 +217,12 @@ def contains(self, item, prereleases=None): # within this Specifier or not. operator_callable = self._get_operator( self.operator - ) + ) # type: Callable[[ParsedVersion, str], bool] return operator_callable(normalized_item, self.version) def filter(self, iterable, prereleases=None): + # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion] + yielded = False found_prereleases = [] @@ -236,32 +281,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) @@ -379,6 +435,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 @@ -406,6 +464,8 @@ def _compare_compatible(self, prospective, spec): @_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. @@ -445,18 +505,23 @@ def _compare_equal(self, prospective, spec): @_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): + # type: (ParsedVersion, str) -> bool return prospective <= Version(spec) @_require_version_compare def _compare_greater_than_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool return prospective >= Version(spec) @_require_version_compare 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_str) @@ -482,6 +547,8 @@ def _compare_less_than(self, prospective, spec_str): @_require_version_compare 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_str) @@ -512,10 +579,13 @@ def _compare_greater_than(self, prospective, spec_str): 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: @@ -540,6 +610,7 @@ def prereleases(self): @prereleases.setter def prereleases(self, value): + # type: (bool) -> None self._prereleases = value @@ -547,7 +618,8 @@ def prereleases(self, value): 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: @@ -558,6 +630,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 @@ -577,6 +650,8 @@ def _pad_version(left, right): class SpecifierSet(BaseSpecifier): def __init__(self, specifiers="", prereleases=None): + # type: (str, Optional[bool]) -> None + # Split on , to break each indidivual specifier into it's own item, and # strip each item to remove leading/trailing whitespace. split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] @@ -598,6 +673,7 @@ def __init__(self, specifiers="", prereleases=None): self._prereleases = prereleases def __repr__(self): + # type: () -> str pre = ( ", prereleases={0!r}".format(self.prereleases) if self._prereleases is not None @@ -607,12 +683,15 @@ def __repr__(self): return "".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): @@ -636,6 +715,7 @@ def __and__(self, other): return specifier def __eq__(self, other): + # type: (object) -> bool if isinstance(other, (string_types, _IndividualSpecifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): @@ -644,6 +724,7 @@ def __eq__(self, other): return self._specs == other._specs def __ne__(self, other): + # type: (object) -> bool if isinstance(other, (string_types, _IndividualSpecifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): @@ -652,13 +733,17 @@ def __ne__(self, other): 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: @@ -676,12 +761,16 @@ def prereleases(self): @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) @@ -707,7 +796,13 @@ def contains(self, item, prereleases=None): # 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. @@ -725,8 +820,8 @@ def filter(self, iterable, prereleases=None): # 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. diff --git a/packaging/tags.py b/packaging/tags.py index 6c8a7962d..9ede8ef32 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -19,6 +19,10 @@ import sysconfig import warnings +from ._typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import Dict, FrozenSet, Iterator, List, Optional, Tuple INTERPRETER_SHORT_NAMES = { "python": "py", # Generic. @@ -26,7 +30,7 @@ "pypy": "pp", "ironpython": "ip", "jython": "jy", -} +} # type: Dict[str, str] _32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 @@ -37,23 +41,28 @@ class Tag(object): __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 @@ -64,16 +73,20 @@ def __eq__(self, other): ) 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] tags = set() interpreters, abis, platforms = tag.split("-") for interpreter in interpreters.split("."): @@ -84,15 +97,18 @@ def parse_tag(tag): def _normalize_string(string): + # type: (str) -> str return string.replace(".", "_").replace("-", "_") def _cpython_interpreter(py_version): + # type: (Tuple[int, int]) -> str # TODO: Is using py_version_nodot for interpreter version critical? return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) def _cpython_abis(py_version): + # type: (Tuple[int, int]) -> str abis = [] version = "{}{}".format(*py_version[:2]) debug = pymalloc = ucs4 = "" @@ -128,6 +144,7 @@ def _cpython_abis(py_version): def _cpython_tags(py_version, interpreter, abis, platforms): + # type: (Tuple[int, int], str, str, List[str]) -> Iterator[Tag] for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) @@ -145,6 +162,7 @@ def _cpython_tags(py_version, interpreter, abis, platforms): def _pypy_interpreter(): + # type: () -> str return "pp{py_major}{pypy_major}{pypy_minor}".format( py_major=sys.version_info[0], pypy_major=sys.pypy_version_info.major, @@ -153,6 +171,7 @@ def _pypy_interpreter(): def _generic_abi(): + # type: () -> str abi = sysconfig.get_config_var("SOABI") if abi: return _normalize_string(abi) @@ -161,6 +180,7 @@ def _generic_abi(): def _pypy_tags(py_version, interpreter, abi, platforms): + # type: (Tuple[int, int], str, str, List[str]) -> Iterator[Tag] for tag in (Tag(interpreter, abi, platform) for platform in platforms): yield tag for tag in (Tag(interpreter, "none", platform) for platform in platforms): @@ -168,6 +188,7 @@ def _pypy_tags(py_version, interpreter, abi, platforms): def _generic_tags(interpreter, py_version, abi, platforms): + # type: (str, Tuple[int, int], str, List[str]) -> Iterator[Tag] for tag in (Tag(interpreter, abi, platform) for platform in platforms): yield tag if abi != "none": @@ -177,6 +198,7 @@ def _generic_tags(interpreter, py_version, abi, platforms): def _py_interpreter_range(py_version): + # type: (Tuple[int, int]) -> Iterator[str] """ Yield Python versions in descending order. @@ -190,6 +212,7 @@ def _py_interpreter_range(py_version): def _independent_tags(interpreter, py_version, platforms): + # type: (str, Tuple[int, int], List[str]) -> Iterator[Tag] """ Return the sequence of tags that are consistent across implementations. @@ -207,6 +230,7 @@ def _independent_tags(interpreter, py_version, platforms): def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): + # type: (str, bool) -> str if not is_32bit: return arch @@ -217,6 +241,7 @@ def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): def _mac_binary_formats(version, cpu_arch): + # type: (Tuple[int, int], str) -> List[str] formats = [cpu_arch] if cpu_arch == "x86_64": if version < (10, 4): @@ -243,7 +268,11 @@ def _mac_binary_formats(version, cpu_arch): return formats -def _mac_platforms(version=None, arch=None): +def _mac_platforms( + version=None, # type: Optional[Tuple[int, int]] + arch=None, # type: Optional[str] +): + # type: (...) -> List[str] version_str, _, cpu_arch = platform.mac_ver() if version is None: _version = tuple(map(int, version_str.split(".")[:2])) @@ -270,6 +299,7 @@ def _mac_platforms(version=None, arch=None): # From PEP 513. def _is_manylinux_compatible(name, glibc_version): + # type: (str, Tuple[int, int]) -> bool # Check for presence of _manylinux module. try: import _manylinux @@ -283,6 +313,7 @@ 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 @@ -290,7 +321,9 @@ def _glibc_version_string(): # 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: @@ -300,7 +333,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") @@ -310,6 +343,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 @@ -331,6 +365,7 @@ 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 @@ -338,6 +373,7 @@ def _have_compatible_glibc(required_major, minimum_minor): def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): + # type: (bool) -> List[str] linux = _normalize_string(distutils.util.get_platform()) if linux == "linux_x86_64" and is_32bit: linux = "linux_i686" @@ -360,16 +396,19 @@ def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): def _generic_platforms(): + # type: () -> List[str] platform = _normalize_string(distutils.util.get_platform()) return [platform] def _interpreter_name(): + # type: () -> str name = platform.python_implementation().lower() return INTERPRETER_SHORT_NAMES.get(name) or name def _generic_interpreter(name, py_version): + # type: (str, Tuple[int, int]) -> str version = sysconfig.get_config_var("py_version_nodot") if not version: version = "".join(map(str, py_version[:2])) @@ -377,6 +416,7 @@ def _generic_interpreter(name, py_version): def sys_tags(): + # type: () -> Iterator[Tag] """ Returns the sequence of tag triples for the running interpreter. diff --git a/packaging/utils.py b/packaging/utils.py index 6b97824f2..545772f72 100644 --- a/packaging/utils.py +++ b/packaging/utils.py @@ -5,18 +5,23 @@ import re +from ._typing import MYPY_CHECK_RUNNING from .version import InvalidVersion, Version +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import Union _canonicalize_regex = re.compile(r"[-_.]+") def canonicalize_name(name): + # type: (str) -> str # This is taken from PEP 503. return _canonicalize_regex.sub("-", name).lower() def canonicalize_version(_version): + # type: (str) -> Union[Version, str] """ This is very similar to Version.__str__, but has one subtle differences with the way it handles the release segment. diff --git a/packaging/version.py b/packaging/version.py index 8c9728ec5..b1aff6558 100644 --- a/packaging/version.py +++ b/packaging/version.py @@ -8,7 +8,34 @@ import re from ._structures import Infinity, NegativeInfinity - +from ._typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: # 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 @@ 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,30 +65,38 @@ class InvalidVersion(ValueError): class _BaseVersion(object): - _key = None + _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 @@ -69,57 +105,71 @@ def _compare(self, other, method): 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 "".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 @@ -135,6 +185,7 @@ def is_devrelease(self): 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) @@ -152,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, @@ -160,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 @@ -216,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: @@ -244,9 +299,11 @@ def __init__(self, version): ) def __repr__(self): + # type: () -> str return "".format(repr(str(self))) def __str__(self): + # type: () -> str parts = [] # Epoch @@ -276,29 +333,35 @@ def __str__(self): @property def epoch(self): - _epoch = self._version.epoch + # type: () -> int + _epoch = self._version.epoch # type: int return _epoch @property def release(self): - _release = self._version.release + # type: () -> Tuple[int, ...] + _release = self._version.release # type: Tuple[int, ...] return _release @property def pre(self): - _pre = 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: @@ -306,10 +369,12 @@ def local(self): @property def public(self): + # type: () -> str return str(self).split("+", 1)[0] @property def base_version(self): + # type: () -> str parts = [] # Epoch @@ -323,18 +388,26 @@ def base_version(self): @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 -def _parse_letter_version(letter, number): +def _parse_letter_version( + letter, # type: str + number, # type: Union[str, bytes, SupportsInt] +): + # type: (...) -> Optional[Tuple[str, int]] + if letter: # We consider there to be an implicit 0 in a pre-release if there is # not a numeral associated with it. @@ -371,6 +444,7 @@ def _parse_letter_version(letter, number): def _parse_local_version(local): + # type: (str) -> Optional[LocalType] """ Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). """ @@ -382,8 +456,16 @@ def _parse_local_version(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 @@ -398,7 +480,7 @@ 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 = NegativeInfinity + _pre = NegativeInfinity # type: PrePostDevType # Versions without a pre-release (except as noted above) should sort after # those with one. elif pre is None: @@ -408,21 +490,21 @@ def _cmpkey(epoch, release, pre, post, dev, local): # Versions without a post segment should sort before those with one. if post is None: - _post = NegativeInfinity + _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 = NegativeInfinity + _local = NegativeInfinity # type: LocalType else: # Versions with a local segment need that segment parsed to implement # the sorting rules in PEP440. @@ -432,8 +514,7 @@ def _cmpkey(epoch, release, pre, post, dev, local): # - Shorter versions sort before longer versions when the prefixes # match exactly _local = tuple( - (i, "") if isinstance(i, int) else (NegativeInfinity, i) - for i in local + (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local ) return epoch, _release, _pre, _post, _dev, _local diff --git a/stubs/platform.pyi b/stubs/platform.pyi new file mode 100644 index 000000000..bf118f422 --- /dev/null +++ b/stubs/platform.pyi @@ -0,0 +1,8 @@ +# fmt: off +# (see https://github.com/psf/black/issues/1020) + +from typing import Any, Tuple + +def mac_ver(release=..., versioninfo=..., machine=...): + # type: (Any, Any, Any) -> Tuple[str, Tuple[str, str, str], str] + ... From d717dcd5409a0c73d90c621e81063b99d1865594 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 17 Sep 2019 15:34:50 +0900 Subject: [PATCH 008/114] Create 'CallableOperator' type alias --- packaging/specifiers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index 091e9de7a..b366978e3 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -27,6 +27,7 @@ ParsedVersion = Union[Version, LegacyVersion] UnparsedVersion = Union[Version, LegacyVersion, str] + CallableOperator = Callable[[ParsedVersion, str], bool] class InvalidSpecifier(ValueError): @@ -160,10 +161,10 @@ def __ne__(self, other): return self._spec != other._spec def _get_operator(self, op): - # type: (str) -> Callable[[ParsedVersion, str], bool] + # type: (str) -> CallableOperator operator_callable = getattr( self, "_compare_{0}".format(self._operators[op]) - ) # type: Callable[[ParsedVersion, str], bool] + ) # type: CallableOperator return operator_callable def _coerce_version(self, version): @@ -215,9 +216,7 @@ def contains(self, item, prereleases=None): # Actually do the comparison to determine if this item is contained # within this Specifier or not. - operator_callable = self._get_operator( - self.operator - ) # type: Callable[[ParsedVersion, str], bool] + operator_callable = self._get_operator(self.operator) # type: CallableOperator return operator_callable(normalized_item, self.version) def filter(self, iterable, prereleases=None): From 47c629b7f090596b266ff3063aaf060b71d2467d Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 17 Sep 2019 16:40:42 +0900 Subject: [PATCH 009/114] Cast to remove Optional instead --- packaging/tags.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packaging/tags.py b/packaging/tags.py index 9ede8ef32..13363f1a9 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -22,7 +22,13 @@ from ._typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: # pragma: no cover - from typing import Dict, FrozenSet, Iterator, List, Optional, Tuple + from typing import cast, Dict, FrozenSet, Iterator, List, Optional, Tuple +else: + # typing's cast() is needed at runtime, but we don't want to import typing. + # Thus, we use a dummy no-op version, which we tell mypy to ignore. + def cast(type_, value): # type: ignore + return value + INTERPRETER_SHORT_NAMES = { "python": "py", # Generic. @@ -275,17 +281,17 @@ def _mac_platforms( # type: (...) -> List[str] version_str, _, cpu_arch = platform.mac_ver() if version is None: - _version = tuple(map(int, version_str.split(".")[:2])) + version = cast("Tuple[int, int]", tuple(map(int, version_str.split(".")[:2]))) else: - _version = version + version = version if arch is None: - _arch = _mac_arch(cpu_arch) + arch = _mac_arch(cpu_arch) else: - _arch = arch + arch = arch platforms = [] - for minor_version in range(_version[1], -1, -1): - compat_version = _version[0], minor_version - binary_formats = _mac_binary_formats(compat_version, _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( From a44767e897e98abf015b38b8943cbff2884e09ce Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 19 Sep 2019 17:30:39 +0800 Subject: [PATCH 010/114] Update types from #199 --- packaging/tags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/tags.py b/packaging/tags.py index 13363f1a9..9402636c2 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -114,7 +114,7 @@ def _cpython_interpreter(py_version): def _cpython_abis(py_version): - # type: (Tuple[int, int]) -> str + # type: (Tuple[int, int]) -> List[str] abis = [] version = "{}{}".format(*py_version[:2]) debug = pymalloc = ucs4 = "" @@ -150,7 +150,7 @@ def _cpython_abis(py_version): def _cpython_tags(py_version, interpreter, abis, platforms): - # type: (Tuple[int, int], str, str, List[str]) -> Iterator[Tag] + # type: (Tuple[int, int], str, List[str], List[str]) -> Iterator[Tag] for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) From 903dc8866f7a2a3332ba136a5f7841e630f0d778 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 19 Sep 2019 17:58:22 +0800 Subject: [PATCH 011/114] Remove custom stubs --- MANIFEST.in | 1 - packaging/tags.py | 2 +- setup.cfg | 19 ++++++++++++++++++- stubs/platform.pyi | 8 -------- tox.ini | 4 ++-- 5 files changed, 21 insertions(+), 13 deletions(-) delete mode 100644 stubs/platform.pyi diff --git a/MANIFEST.in b/MANIFEST.in index c9804a4be..36fcbd8e6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,7 +7,6 @@ include tox.ini recursive-include docs * recursive-include tests *.py -recursive-include stubs * exclude .travis.yml exclude dev-requirements.txt diff --git a/packaging/tags.py b/packaging/tags.py index 9402636c2..d47961518 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -279,7 +279,7 @@ def _mac_platforms( arch=None, # type: Optional[str] ): # type: (...) -> List[str] - version_str, _, cpu_arch = platform.mac_ver() + version_str, _, cpu_arch = platform.mac_ver() # type: ignore if version is None: version = cast("Tuple[int, int]", tuple(map(int, version_str.split(".")[:2]))) else: diff --git a/setup.cfg b/setup.cfg index 6cfa4257e..c67cd2261 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,4 +3,21 @@ universal=1 [mypy] ignore_missing_imports = True -mypy_path = stubs + +# The following are the flags enabled by --strict +# +# Note: warn_unused_ignores is False due to incorrect typeshed annotations for +# platform.mac_ver() +warn_unused_configs = True +disallow_subclassing_any = True +disallow_any_generics = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +check_untyped_defs = True +disallow_untyped_decorators = True +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = False +warn_return_any = True +no_implicit_reexport = True diff --git a/stubs/platform.pyi b/stubs/platform.pyi deleted file mode 100644 index bf118f422..000000000 --- a/stubs/platform.pyi +++ /dev/null @@ -1,8 +0,0 @@ -# fmt: off -# (see https://github.com/psf/black/issues/1020) - -from typing import Any, Tuple - -def mac_ver(release=..., versioninfo=..., machine=...): - # type: (Any, Any, Any) -> Tuple[str, Tuple[str, str, str], str] - ... diff --git a/tox.ini b/tox.ini index 3de45e221..6ce3cf963 100644 --- a/tox.ini +++ b/tox.ini @@ -34,8 +34,8 @@ basepython = python3 deps = mypy commands = - mypy packaging --strict - mypy packaging --py2 --strict + mypy packaging + mypy packaging --py2 [testenv:lint] basepython = python3 From 8debe32457d86f33b10363484892dd80691916d2 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 23 Sep 2019 12:02:35 -0700 Subject: [PATCH 012/114] Move dummy cast() to _typing --- packaging/_typing.py | 10 ++++++++++ packaging/tags.py | 9 ++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packaging/_typing.py b/packaging/_typing.py index 576770e6c..dc6dfce7a 100644 --- a/packaging/_typing.py +++ b/packaging/_typing.py @@ -27,3 +27,13 @@ """ MYPY_CHECK_RUNNING = False + +if MYPY_CHECK_RUNNING: # pragma: no cover + import typing + + cast = typing.cast +else: + # typing's cast() is needed at runtime, but we don't want to import typing. + # Thus, we use a dummy no-op version, which we tell mypy to ignore. + def cast(type_, value): # type: ignore + return value diff --git a/packaging/tags.py b/packaging/tags.py index d47961518..6d3554cbe 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -19,15 +19,10 @@ import sysconfig import warnings -from ._typing import MYPY_CHECK_RUNNING +from ._typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: # pragma: no cover - from typing import cast, Dict, FrozenSet, Iterator, List, Optional, Tuple -else: - # typing's cast() is needed at runtime, but we don't want to import typing. - # Thus, we use a dummy no-op version, which we tell mypy to ignore. - def cast(type_, value): # type: ignore - return value + from typing import Dict, FrozenSet, Iterator, List, Optional, Tuple INTERPRETER_SHORT_NAMES = { From 2009329dc7d95a70504acccf02374096daa8f79d Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 23 Sep 2019 12:05:07 -0700 Subject: [PATCH 013/114] Alphabetize imports --- packaging/markers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/markers.py b/packaging/markers.py index 45b7e2a38..11d140e08 100644 --- a/packaging/markers.py +++ b/packaging/markers.py @@ -17,7 +17,7 @@ from .specifiers import Specifier, InvalidSpecifier if MYPY_CHECK_RUNNING: # pragma: no cover - from typing import Any, List, Tuple, Dict, Union, Optional, Callable + from typing import Any, Callable, Dict, List, Optional, Tuple, Union Operator = Callable[[str, str], bool] From b1a8ec343b603546a2252f781609f4cb9740bfc2 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 23 Sep 2019 12:08:09 -0700 Subject: [PATCH 014/114] Add aliases for version types --- packaging/tags.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packaging/tags.py b/packaging/tags.py index 6d3554cbe..ca830068f 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -24,6 +24,10 @@ if MYPY_CHECK_RUNNING: # pragma: no cover from typing import Dict, FrozenSet, Iterator, List, Optional, Tuple + PythonVersion = Tuple[int, int] + MacVersion = Tuple[int, int] + GlibcVersion = Tuple[int, int] + INTERPRETER_SHORT_NAMES = { "python": "py", # Generic. @@ -103,13 +107,13 @@ def _normalize_string(string): def _cpython_interpreter(py_version): - # type: (Tuple[int, int]) -> str + # type: (PythonVersion) -> str # TODO: Is using py_version_nodot for interpreter version critical? return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) def _cpython_abis(py_version): - # type: (Tuple[int, int]) -> List[str] + # type: (PythonVersion) -> List[str] abis = [] version = "{}{}".format(*py_version[:2]) debug = pymalloc = ucs4 = "" @@ -145,7 +149,7 @@ def _cpython_abis(py_version): def _cpython_tags(py_version, interpreter, abis, platforms): - # type: (Tuple[int, int], str, List[str], List[str]) -> Iterator[Tag] + # type: (PythonVersion, str, List[str], List[str]) -> Iterator[Tag] for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) @@ -181,7 +185,7 @@ def _generic_abi(): def _pypy_tags(py_version, interpreter, abi, platforms): - # type: (Tuple[int, int], str, str, List[str]) -> Iterator[Tag] + # type: (PythonVersion, str, str, List[str]) -> Iterator[Tag] for tag in (Tag(interpreter, abi, platform) for platform in platforms): yield tag for tag in (Tag(interpreter, "none", platform) for platform in platforms): @@ -189,7 +193,7 @@ def _pypy_tags(py_version, interpreter, abi, platforms): def _generic_tags(interpreter, py_version, abi, platforms): - # type: (str, Tuple[int, int], str, List[str]) -> Iterator[Tag] + # type: (str, PythonVersion, str, List[str]) -> Iterator[Tag] for tag in (Tag(interpreter, abi, platform) for platform in platforms): yield tag if abi != "none": @@ -199,7 +203,7 @@ def _generic_tags(interpreter, py_version, abi, platforms): def _py_interpreter_range(py_version): - # type: (Tuple[int, int]) -> Iterator[str] + # type: (PythonVersion) -> Iterator[str] """ Yield Python versions in descending order. @@ -213,7 +217,7 @@ def _py_interpreter_range(py_version): def _independent_tags(interpreter, py_version, platforms): - # type: (str, Tuple[int, int], List[str]) -> Iterator[Tag] + # type: (str, PythonVersion, List[str]) -> Iterator[Tag] """ Return the sequence of tags that are consistent across implementations. @@ -242,7 +246,7 @@ def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): def _mac_binary_formats(version, cpu_arch): - # type: (Tuple[int, int], str) -> List[str] + # type: (MacVersion, str) -> List[str] formats = [cpu_arch] if cpu_arch == "x86_64": if version < (10, 4): @@ -270,13 +274,13 @@ def _mac_binary_formats(version, cpu_arch): def _mac_platforms( - version=None, # type: Optional[Tuple[int, int]] + version=None, # type: Optional[MacVersion] arch=None, # type: Optional[str] ): # type: (...) -> List[str] version_str, _, cpu_arch = platform.mac_ver() # type: ignore if version is None: - version = cast("Tuple[int, int]", tuple(map(int, version_str.split(".")[:2]))) + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) else: version = version if arch is None: @@ -300,7 +304,7 @@ def _mac_platforms( # From PEP 513. def _is_manylinux_compatible(name, glibc_version): - # type: (str, Tuple[int, int]) -> bool + # type: (str, GlibcVersion) -> bool # Check for presence of _manylinux module. try: import _manylinux @@ -409,7 +413,7 @@ def _interpreter_name(): def _generic_interpreter(name, py_version): - # type: (str, Tuple[int, int]) -> str + # type: (str, PythonVersion) -> str version = sysconfig.get_config_var("py_version_nodot") if not version: version = "".join(map(str, py_version[:2])) From b30a663864729df01880278f620d7497705b48ab Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 23 Sep 2019 12:10:31 -0700 Subject: [PATCH 015/114] Loosen List/Iterator strictness --- packaging/tags.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packaging/tags.py b/packaging/tags.py index ca830068f..ce8bce6ef 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -22,7 +22,7 @@ from ._typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: # pragma: no cover - from typing import Dict, FrozenSet, Iterator, List, Optional, Tuple + from typing import Dict, FrozenSet, Iterable, Iterator, List, Optional, Tuple PythonVersion = Tuple[int, int] MacVersion = Tuple[int, int] @@ -149,7 +149,7 @@ def _cpython_abis(py_version): def _cpython_tags(py_version, interpreter, abis, platforms): - # type: (PythonVersion, str, List[str], List[str]) -> Iterator[Tag] + # type: (PythonVersion, str, Iterable[str], Iterable[str]) -> Iterator[Tag] for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) @@ -185,7 +185,7 @@ def _generic_abi(): def _pypy_tags(py_version, interpreter, abi, platforms): - # type: (PythonVersion, str, str, List[str]) -> Iterator[Tag] + # type: (PythonVersion, str, str, Iterable[str]) -> Iterator[Tag] for tag in (Tag(interpreter, abi, platform) for platform in platforms): yield tag for tag in (Tag(interpreter, "none", platform) for platform in platforms): @@ -193,7 +193,7 @@ def _pypy_tags(py_version, interpreter, abi, platforms): def _generic_tags(interpreter, py_version, abi, platforms): - # type: (str, PythonVersion, str, List[str]) -> Iterator[Tag] + # type: (str, PythonVersion, str, Iterable[str]) -> Iterator[Tag] for tag in (Tag(interpreter, abi, platform) for platform in platforms): yield tag if abi != "none": @@ -217,7 +217,7 @@ def _py_interpreter_range(py_version): def _independent_tags(interpreter, py_version, platforms): - # type: (str, PythonVersion, List[str]) -> Iterator[Tag] + # type: (str, PythonVersion, Iterable[str]) -> Iterator[Tag] """ Return the sequence of tags that are consistent across implementations. From 4e480cb747d852b65e354d4b903d853a825d786f Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Wed, 25 Sep 2019 12:33:57 -0700 Subject: [PATCH 016/114] Create a GitHub action for testing Initially assumes x64 architecture. --- .github/workflows/test.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..580da3db3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Test + +on: [push] + +jobs: + test: + name: Test Python ${{ python-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + max-parallel: 4 + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + python-version: [pypy, pypy3, 2.7, 3.4, 3.5, 3.6, 3.7] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r dev-requirements.txt + - name: Test coverage + if: !startsWith(${{ matrix.python-version }}, "pypy") + run: | + python -m coverage run --source packaging/ -m pytest --strict + python -m coverage report -m --fail-under 100 + - name: Test ${{ matrix.python-version }} + if: startsWith(${{ matrix.python-version }}, "pypy") + run: python -m pytest --capture=no --strict From 9805b57480db447c78ceea620f59ecf9e55742e7 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 09:43:49 -0700 Subject: [PATCH 017/114] Attempt to fix syntax error in GH action --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 580da3db3..f1ddefe09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,10 +24,10 @@ jobs: python -m pip install --upgrade pip python -m pip install -r dev-requirements.txt - name: Test coverage - if: !startsWith(${{ matrix.python-version }}, "pypy") + if: !startsWith(matrix.python-version, "pypy") run: | python -m coverage run --source packaging/ -m pytest --strict python -m coverage report -m --fail-under 100 - name: Test ${{ matrix.python-version }} - if: startsWith(${{ matrix.python-version }}, "pypy") + if: startsWith(matrix.python-version, "pypy") run: python -m pytest --capture=no --strict From 8e1b63538280a3ce36689ed35b711f1ee0d83368 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 09:46:17 -0700 Subject: [PATCH 018/114] Fix a variable reference --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1ddefe09..90fefe5cb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: [push] jobs: test: - name: Test Python ${{ python-version }} on ${{ matrix.os }} + name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: From 232a9ec6c9c0fb2e6e262ce1bf8f6e743e50192b Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 09:49:15 -0700 Subject: [PATCH 019/114] Drop PyPy from GH testing action (for now) To try to fix persistent YAML format errors. --- .github/workflows/test.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 90fefe5cb..75c4a44fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - python-version: [pypy, pypy3, 2.7, 3.4, 3.5, 3.6, 3.7] + python-version: [2.7, 3.4, 3.5, 3.6, 3.7] steps: - uses: actions/checkout@v1 @@ -24,10 +24,6 @@ jobs: python -m pip install --upgrade pip python -m pip install -r dev-requirements.txt - name: Test coverage - if: !startsWith(matrix.python-version, "pypy") run: | python -m coverage run --source packaging/ -m pytest --strict python -m coverage report -m --fail-under 100 - - name: Test ${{ matrix.python-version }} - if: startsWith(matrix.python-version, "pypy") - run: python -m pytest --capture=no --strict From 609cc5e84757a50b7e9ccf854657f43c58b46c2a Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 09:54:17 -0700 Subject: [PATCH 020/114] Add some newlines to GH action --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75c4a44fe..5ef00247c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,10 +19,12 @@ jobs: uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -r dev-requirements.txt + - name: Test coverage run: | python -m coverage run --source packaging/ -m pytest --strict From 583c166299918cba7c386efdc68a1b8d30a48b49 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 09:58:34 -0700 Subject: [PATCH 021/114] Drop Python 3.4 from GH action Not currently available out-of-the-box (but could get through a Docker container). --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ef00247c..5922fb96a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,11 +11,12 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - python-version: [2.7, 3.4, 3.5, 3.6, 3.7] + # Python 3.4 is not available. + python-version: [2.7, 3.5, 3.6, 3.7] steps: - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} From cfdc3369b1395298fe4d466fc753958728dd4f12 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:08:23 -0700 Subject: [PATCH 022/114] Trying adding back in pypy support in the GH action --- .github/workflows/test.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5922fb96a..c95dd6335 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,13 +11,13 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - # Python 3.4 is not available. - python-version: [2.7, 3.5, 3.6, 3.7] + # Python 3.4 is not available as part of actions/setup-python@v1. + python-version: [pypy, pypy3, 2.7, 3.5, 3.6, 3.7] steps: - uses: actions/checkout@v1 - - name: Set up Python - uses: actions/setup-python@v1 + + - uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} @@ -27,6 +27,11 @@ jobs: python -m pip install -r dev-requirements.txt - name: Test coverage + if: !startsWith(matrix.python-version, "pypy") run: | python -m coverage run --source packaging/ -m pytest --strict python -m coverage report -m --fail-under 100 + + - name: Test pypy with pytest + if: startsWith(matrix-python-version, "pypy") + run: python -m pytest --capture=no --strict From c661fb65f19349667d474f4c2015a82140284df5 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:09:14 -0700 Subject: [PATCH 023/114] Fix a typo --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c95dd6335..a6c47d8ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,5 +33,5 @@ jobs: python -m coverage report -m --fail-under 100 - name: Test pypy with pytest - if: startsWith(matrix-python-version, "pypy") + if: startsWith(matrix.python-version, "pypy") run: python -m pytest --capture=no --strict From c7c876f562d45da7c4beb7d0533e220d778ecfbc Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:12:05 -0700 Subject: [PATCH 024/114] Last attempt to make PyPy work in a single action file --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6c47d8ed..09bd1d69a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: [push] jobs: test: - name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} + name: Test Python ${{ matrix.python_version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: @@ -12,14 +12,14 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] # Python 3.4 is not available as part of actions/setup-python@v1. - python-version: [pypy, pypy3, 2.7, 3.5, 3.6, 3.7] + python_version: [pypy, pypy3, 2.7, 3.5, 3.6, 3.7] steps: - uses: actions/checkout@v1 - uses: actions/setup-python@v1 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ matrix.python_version }} - name: Install dependencies run: | @@ -27,11 +27,11 @@ jobs: python -m pip install -r dev-requirements.txt - name: Test coverage - if: !startsWith(matrix.python-version, "pypy") + if: matrix.python_version != "pypy" || matrix.python_version != "pypy3" run: | python -m coverage run --source packaging/ -m pytest --strict python -m coverage report -m --fail-under 100 - name: Test pypy with pytest - if: startsWith(matrix.python-version, "pypy") + if: startsWith(matrix.python_version, "pypy") run: python -m pytest --capture=no --strict From f47f91a629012d57420e4c38b4982282f41fe80a Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:13:20 -0700 Subject: [PATCH 025/114] Use single quotes in GH action --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09bd1d69a..9c15c6e78 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,11 +27,11 @@ jobs: python -m pip install -r dev-requirements.txt - name: Test coverage - if: matrix.python_version != "pypy" || matrix.python_version != "pypy3" + if: matrix.python_version != 'pypy' || matrix.python_version != 'pypy3' run: | python -m coverage run --source packaging/ -m pytest --strict python -m coverage report -m --fail-under 100 - name: Test pypy with pytest - if: startsWith(matrix.python_version, "pypy") + if: startsWith(matrix.python_version, 'pypy') run: python -m pytest --capture=no --strict From 3c9b57a484ff09309c1c7b7a7c57fbe7c1f90bf0 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:20:03 -0700 Subject: [PATCH 026/114] Limit pypy to x86 --- .github/workflows/test.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c15c6e78..0e39478fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,11 +8,15 @@ jobs: runs-on: ${{ matrix.os }} strategy: - max-parallel: 4 matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - # Python 3.4 is not available as part of actions/setup-python@v1. - python_version: [pypy, pypy3, 2.7, 3.5, 3.6, 3.7] + # Python 3.4 is not available from actions/setup-python@v1. + python_version: [pypy3, 2.7, 3.5, 3.6, 3.7] + architecture: x64 + include: + python_version: pypy + # pypy on x64 not available from actions/setup-python@v1. + architecture: x86 steps: - uses: actions/checkout@v1 @@ -20,6 +24,7 @@ jobs: - uses: actions/setup-python@v1 with: python-version: ${{ matrix.python_version }} + architecture: ${{ matrix.architecture }} - name: Install dependencies run: | @@ -27,7 +32,7 @@ jobs: python -m pip install -r dev-requirements.txt - name: Test coverage - if: matrix.python_version != 'pypy' || matrix.python_version != 'pypy3' + if: !startsWith(matrix.python_version, 'pypy') run: | python -m coverage run --source packaging/ -m pytest --strict python -m coverage report -m --fail-under 100 From b1e0ffb3c54bcb11df45978ac6e4fb11f0e0a11e Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:20:45 -0700 Subject: [PATCH 027/114] Fix pypy check in GH action --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e39478fb..92c2fad3b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: python -m pip install -r dev-requirements.txt - name: Test coverage - if: !startsWith(matrix.python_version, 'pypy') + if: matrix.python_version != 'pypy' || matrix.python_version != 'pypy3' run: | python -m coverage run --source packaging/ -m pytest --strict python -m coverage report -m --fail-under 100 From 1c729f0e879f1ba9a1777080d9aa20fd79eec6eb Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:23:58 -0700 Subject: [PATCH 028/114] Make 'architecture' an array in GH action --- .github/workflows/test.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92c2fad3b..fee0b2489 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,17 +4,17 @@ on: [push] jobs: test: - name: Test Python ${{ matrix.python_version }} on ${{ matrix.os }} + name: Test Python ${{ matrix.python_version }} on ${{ matrix.os }} w/ ${{ matrix.architecture }} interpreter runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] # Python 3.4 is not available from actions/setup-python@v1. - python_version: [pypy3, 2.7, 3.5, 3.6, 3.7] - architecture: x64 + python_version: ['pypy3', '2.7', '3.5', '3.6', '3.7'] + architecture: ['x64'] include: - python_version: pypy + python_version: 'pypy' # pypy on x64 not available from actions/setup-python@v1. architecture: x86 @@ -22,6 +22,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-python@v1 + name: Install Python ${{ matrix.python_version}} ${{ matrix.architecture }} with: python-version: ${{ matrix.python_version }} architecture: ${{ matrix.architecture }} From e086186be9a15895c51857058f0e736d9f0a1736 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:25:18 -0700 Subject: [PATCH 029/114] Use a list instead of mapping for 'includes' --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fee0b2489..9ec6885a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,15 +14,15 @@ jobs: python_version: ['pypy3', '2.7', '3.5', '3.6', '3.7'] architecture: ['x64'] include: - python_version: 'pypy' - # pypy on x64 not available from actions/setup-python@v1. - architecture: x86 + - python_version: 'pypy' + # pypy on x64 not available from actions/setup-python@v1. + architecture: x86 steps: - uses: actions/checkout@v1 - uses: actions/setup-python@v1 - name: Install Python ${{ matrix.python_version}} ${{ matrix.architecture }} + name: Install Python ${{ matrix.python_version}} (${{ matrix.architecture }}) with: python-version: ${{ matrix.python_version }} architecture: ${{ matrix.architecture }} From bd6b9302c0a6c644eaf7f5d44694543853b6f47e Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 10:54:44 -0700 Subject: [PATCH 030/114] Drop PyPy from the GH action Just too messy to try and squeeze it in w/ CPython. --- .github/workflows/test.yml | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9ec6885a5..eb911d949 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,28 +4,22 @@ on: [push] jobs: test: - name: Test Python ${{ matrix.python_version }} on ${{ matrix.os }} w/ ${{ matrix.architecture }} interpreter + name: Test Python ${{ matrix.python_version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] # Python 3.4 is not available from actions/setup-python@v1. - python_version: ['pypy3', '2.7', '3.5', '3.6', '3.7'] - architecture: ['x64'] - include: - - python_version: 'pypy' - # pypy on x64 not available from actions/setup-python@v1. - architecture: x86 + python_version: ['2.7', '3.5', '3.6', '3.7'] steps: - uses: actions/checkout@v1 - uses: actions/setup-python@v1 - name: Install Python ${{ matrix.python_version}} (${{ matrix.architecture }}) + name: Install Python ${{ matrix.python_version }} with: python-version: ${{ matrix.python_version }} - architecture: ${{ matrix.architecture }} - name: Install dependencies run: | @@ -33,11 +27,6 @@ jobs: python -m pip install -r dev-requirements.txt - name: Test coverage - if: matrix.python_version != 'pypy' || matrix.python_version != 'pypy3' run: | python -m coverage run --source packaging/ -m pytest --strict python -m coverage report -m --fail-under 100 - - - name: Test pypy with pytest - if: startsWith(matrix.python_version, 'pypy') - run: python -m pytest --capture=no --strict From 6e9f0d15f7182054af3f7c7ed363b33709984512 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 11:02:44 -0700 Subject: [PATCH 031/114] Break up testing CPython and PyPy into separate actions (#206) --- .../workflows/{test.yml => test-cpython.yml} | 12 +++---- .github/workflows/test-pypy.yml | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) rename .github/workflows/{test.yml => test-cpython.yml} (89%) create mode 100644 .github/workflows/test-pypy.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test-cpython.yml similarity index 89% rename from .github/workflows/test.yml rename to .github/workflows/test-cpython.yml index eb911d949..0d8b37f60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test-cpython.yml @@ -1,4 +1,4 @@ -name: Test +name: Test CPython on: [push] @@ -6,7 +6,7 @@ jobs: test: name: Test Python ${{ matrix.python_version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - + strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] @@ -15,17 +15,17 @@ jobs: steps: - uses: actions/checkout@v1 - + - uses: actions/setup-python@v1 - name: Install Python ${{ matrix.python_version }} + name: Install CPython ${{ matrix.python_version }} with: python-version: ${{ matrix.python_version }} - + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -r dev-requirements.txt - + - name: Test coverage run: | python -m coverage run --source packaging/ -m pytest --strict diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml new file mode 100644 index 000000000..a74be40f5 --- /dev/null +++ b/.github/workflows/test-pypy.yml @@ -0,0 +1,31 @@ +name: Test PyPy + +on: [push] + +jobs: + test: + name: Test ${{ matrix.pypy_version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + pypy_version: ['pypy', 'pypy3'] + + steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-python@v1 + name: Install ${{ matrix.pypy_version }} + with: + python-version: ${{ matrix.pypy_version }} + # pypy for x64 not available from actions/setup-python@v1. + architecture: 'x86' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r dev-requirements.txt + + - name: Run pytest + run: python -m pytest --capture=no --strict From a1be9018743788a1850cf302163c33d183185960 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 26 Sep 2019 11:05:34 -0700 Subject: [PATCH 032/114] Revert "Break up testing CPython and PyPy into separate actions (#206)" (#207) This reverts commit 6e9f0d15f7182054af3f7c7ed363b33709984512. --- .github/workflows/test-pypy.yml | 31 ------------------- .../workflows/{test-cpython.yml => test.yml} | 12 +++---- 2 files changed, 6 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/test-pypy.yml rename .github/workflows/{test-cpython.yml => test.yml} (89%) diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml deleted file mode 100644 index a74be40f5..000000000 --- a/.github/workflows/test-pypy.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Test PyPy - -on: [push] - -jobs: - test: - name: Test ${{ matrix.pypy_version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] - pypy_version: ['pypy', 'pypy3'] - - steps: - - uses: actions/checkout@v1 - - - uses: actions/setup-python@v1 - name: Install ${{ matrix.pypy_version }} - with: - python-version: ${{ matrix.pypy_version }} - # pypy for x64 not available from actions/setup-python@v1. - architecture: 'x86' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -r dev-requirements.txt - - - name: Run pytest - run: python -m pytest --capture=no --strict diff --git a/.github/workflows/test-cpython.yml b/.github/workflows/test.yml similarity index 89% rename from .github/workflows/test-cpython.yml rename to .github/workflows/test.yml index 0d8b37f60..eb911d949 100644 --- a/.github/workflows/test-cpython.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test CPython +name: Test on: [push] @@ -6,7 +6,7 @@ jobs: test: name: Test Python ${{ matrix.python_version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - + strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] @@ -15,17 +15,17 @@ jobs: steps: - uses: actions/checkout@v1 - + - uses: actions/setup-python@v1 - name: Install CPython ${{ matrix.python_version }} + name: Install Python ${{ matrix.python_version }} with: python-version: ${{ matrix.python_version }} - + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -r dev-requirements.txt - + - name: Test coverage run: | python -m coverage run --source packaging/ -m pytest --strict From 3c53aea18be0d87b581d191fa4a248226d2266ed Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Fri, 27 Sep 2019 11:01:47 -0700 Subject: [PATCH 033/114] Appease mypy being strict about sys attributes (#208) --- .gitignore | 1 + packaging/markers.py | 7 +++++-- packaging/tags.py | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 498bce1a4..8250df26f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .idea .venv* +.mypy_cache/ .pytest_cache/ __pycache__/ _build/ diff --git a/packaging/markers.py b/packaging/markers.py index ca6fb9a07..f01747113 100644 --- a/packaging/markers.py +++ b/packaging/markers.py @@ -268,8 +268,11 @@ 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 = "" diff --git a/packaging/tags.py b/packaging/tags.py index ce8bce6ef..c52ec6f2e 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -168,10 +168,12 @@ def _cpython_tags(py_version, interpreter, abis, platforms): def _pypy_interpreter(): # type: () -> str + # Ignoring sys.pypy_version_info for type checking due to typeshed lacking + # the reference to the attribute. 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, + pypy_major=sys.pypy_version_info.major, # type: ignore + pypy_minor=sys.pypy_version_info.minor, # type: ignore ) From 3539508958c2b7d5ea41104b5a8014e88b2d2e5a Mon Sep 17 00:00:00 2001 From: johnthagen Date: Fri, 27 Sep 2019 14:02:37 -0400 Subject: [PATCH 034/114] Document PyPy support in setup.py (#198) --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index b38efb829..d6c6310f7 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,8 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", ], packages=["packaging"], ) From 688e33ed050b3c2ebb24473cbb015f3496420bc8 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Mon, 30 Sep 2019 16:43:51 -0700 Subject: [PATCH 035/114] Introduce pre-commit to the dev process (#212) --- .github/workflows/test.yml | 8 ++--- .pre-commit-config.yaml | 37 ++++++++++++++++++++++ .travis.yml | 2 -- LICENSE.APACHE | 2 +- MANIFEST.in | 1 + docs/Makefile | 2 +- docs/development/reviewing-patches.rst | 2 +- tox.ini | 44 +++++++------------------- 8 files changed, 56 insertions(+), 42 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb911d949..ca51ec5a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ jobs: test: name: Test Python ${{ matrix.python_version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - + strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] @@ -15,17 +15,17 @@ jobs: steps: - uses: actions/checkout@v1 - + - uses: actions/setup-python@v1 name: Install Python ${{ matrix.python_version }} with: python-version: ${{ matrix.python_version }} - + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -r dev-requirements.txt - + - name: Test coverage run: | python -m coverage run --source packaging/ -m pytest --strict diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..367e62e4f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,37 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.720 + hooks: + - id: mypy + exclude: '^(docs|tasks|tests)|setup\.py' + args: [] + - id: mypy + name: mypy for Python 2 + exclude: '^(docs|tasks|tests)|setup\.py' + args: ['--py2'] + +- repo: https://github.com/psf/black + rev: 19.3b0 + hooks: + - id: black + +- repo: https://gitlab.com/PyCQA/flake8 + rev: '3.7.8' + hooks: + - id: flake8 + additional_dependencies: ['pep8-naming'] + # Ignore all format-related checks as Black takes care of those. + args: ['--ignore', 'E2,W5', '--select', 'E,W,F,N'] + +- repo: https://github.com/mgedmin/check-manifest + rev: '0.39' + hooks: + - id: check-manifest diff --git a/.travis.yml b/.travis.yml index 430e77dcb..f82bee88e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,10 +22,8 @@ matrix: - python: 3.8-dev env: TOXENV=py dist: xenial - - env: TOXENV=mypy - env: TOXENV=lint - env: TOXENV=docs - - env: TOXENV=packaging allow_failures: - python: 3.8-dev diff --git a/LICENSE.APACHE b/LICENSE.APACHE index 4947287f7..f433b1a53 100644 --- a/LICENSE.APACHE +++ b/LICENSE.APACHE @@ -174,4 +174,4 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS \ No newline at end of file + END OF TERMS AND CONDITIONS diff --git a/MANIFEST.in b/MANIFEST.in index 36fcbd8e6..b25e07800 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ include LICENSE LICENSE.APACHE LICENSE.BSD include .coveragerc include .flake8 +include .pre-commit-config.yaml include tox.ini recursive-include docs * diff --git a/docs/Makefile b/docs/Makefile index ec2771be2..9d683b402 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -150,4 +150,4 @@ linkcheck: doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." \ No newline at end of file + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/development/reviewing-patches.rst b/docs/development/reviewing-patches.rst index 4f1810c66..c476c7512 100644 --- a/docs/development/reviewing-patches.rst +++ b/docs/development/reviewing-patches.rst @@ -34,4 +34,4 @@ These are small things that are not caught by the automated style checkers. * Does a variable need a better name? * Should this be a keyword argument? -.. _`excellent to one another`: https://speakerdeck.com/ohrite/better-code-review \ No newline at end of file +.. _`excellent to one another`: https://speakerdeck.com/ohrite/better-code-review diff --git a/tox.ini b/tox.ini index 6ce3cf963..9c692587b 100644 --- a/tox.ini +++ b/tox.ini @@ -13,11 +13,20 @@ commands = [testenv:pypy] commands = - py.test --capture=no --strict {posargs} + pytest --capture=no --strict {posargs} [testenv:pypy3] commands = - py.test --capture=no --strict {posargs} + pytest --capture=no --strict {posargs} + +[testenv:lint] +basepython=python3 +deps = + pre-commit + readme_renderer +commands = + pre-commit run --all-files + python setup.py check --metadata --restructuredtext --strict [testenv:docs] basepython = python3 @@ -28,34 +37,3 @@ commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html sphinx-build -W -b latex -d {envtmpdir}/doctrees docs docs/_build/latex sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html - -[testenv:mypy] -basepython = python3 -deps = - mypy -commands = - mypy packaging - mypy packaging --py2 - -[testenv:lint] -basepython = python3 -deps = - flake8 - pep8-naming - black -commands = - flake8 . - black --check . - -[testenv:packaging] -deps = - check-manifest - readme_renderer -commands = - check-manifest - python setup.py check --metadata --restructuredtext --strict - -[flake8] -exclude = .tox,*.egg -select = E,W,F,N -ignore = W504 From 830e095af3f4fdfe6e341b5667b53529e9cfb8b3 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Fri, 27 Sep 2019 14:14:35 -0500 Subject: [PATCH 036/114] Add logging around sysconfig.get_config_var --- packaging/tags.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packaging/tags.py b/packaging/tags.py index c52ec6f2e..0bbd04fa9 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -13,6 +13,7 @@ EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] del imp +import logging import platform import re import sys @@ -22,13 +23,15 @@ from ._typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: # pragma: no cover - from typing import Dict, FrozenSet, Iterable, Iterator, List, Optional, Tuple + from typing import Dict, FrozenSet, Iterable, Iterator, List, Optional, Tuple, Union PythonVersion = Tuple[int, int] MacVersion = Tuple[int, int] GlibcVersion = Tuple[int, int] +logger = logging.getLogger(__name__) + INTERPRETER_SHORT_NAMES = { "python": "py", # Generic. "cpython": "cp", @@ -101,6 +104,16 @@ def parse_tag(tag): return frozenset(tags) +def _get_config_var(name, warn=True): + # type: (str, Optional[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("-", "_") @@ -117,7 +130,7 @@ def _cpython_abis(py_version): abis = [] version = "{}{}".format(*py_version[:2]) debug = pymalloc = ucs4 = "" - with_debug = sysconfig.get_config_var("Py_DEBUG") + with_debug = _get_config_var("Py_DEBUG") has_refcount = hasattr(sys, "gettotalrefcount") # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled # extension modules is the best option. @@ -126,11 +139,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") 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") if unicode_size == 4 or ( unicode_size is None and sys.maxunicode == 0x10FFFF ): @@ -416,7 +429,7 @@ def _interpreter_name(): def _generic_interpreter(name, py_version): # type: (str, PythonVersion) -> str - version = sysconfig.get_config_var("py_version_nodot") + version = _get_config_var("py_version_nodot") if not version: version = "".join(map(str, py_version[:2])) return "{name}{version}".format(name=name, version=version) From 595372dc1075b198be2b0a84a04da01dc40414dc Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Fri, 27 Sep 2019 14:34:34 -0500 Subject: [PATCH 037/114] Port changes to reduce dependencies on ctypes module --- packaging/tags.py | 31 ++++++++++++++++++++++++++++++- tests/test_tags.py | 25 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packaging/tags.py b/packaging/tags.py index 0bbd04fa9..bd03064f5 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -14,6 +14,7 @@ EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] del imp import logging +import os import platform import re import sys @@ -335,7 +336,35 @@ 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("CS_GNU_LIBC_VERSION") + assert version_string is not None + _, version = version_string.split() + 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 diff --git a/tests/test_tags.py b/tests/test_tags.py index 4ad35478c..bf34b65f6 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -10,6 +10,7 @@ ctypes = None import distutils.util +import os import platform import re import sys @@ -17,6 +18,7 @@ import types import warnings +import pretend import pytest from packaging import tags @@ -530,6 +532,29 @@ def __init__(self, libc_version): assert tags._glibc_version_string() is None +def test_glibc_version_string_confstr(monkeypatch): + monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False) + assert tags._glibc_version_string_confstr() == "2.20" + + +@pytest.mark.parametrize( + "failure", [pretend.raiser(ValueError), pretend.raiser(OSError), lambda x: "XXX"] +) +def test_glibc_version_string_confstr_fail(monkeypatch, failure): + monkeypatch.setattr(os, "confstr", failure, raising=False) + assert tags._glibc_version_string_confstr() is None + + +def test_glibc_version_string_confstr_missing(monkeypatch): + monkeypatch.delattr(os, "confstr", raising=False) + assert tags._glibc_version_string_confstr() is None + + +def test_glibc_version_string_ctypes_missing(monkeypatch): + monkeypatch.setitem(sys.modules, "ctypes", None) + assert tags._glibc_version_string_ctypes() is None + + def test_have_compatible_glibc(monkeypatch): if platform.system() == "Linux": # Assuming no one is running this test with a version of glibc released in From eea9e741e2e8f6c4b3926ee0719ea63bf3776809 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Fri, 27 Sep 2019 18:41:03 -0500 Subject: [PATCH 038/114] Add missing monkeypatch --- tests/test_tags.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_tags.py b/tests/test_tags.py index bf34b65f6..59086fe79 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -525,6 +525,7 @@ def __init__(self, libc_version): process_namespace = ProcessNamespace(LibcVersion(version_str)) monkeypatch.setattr(ctypes, "CDLL", lambda _: process_namespace) + monkeypatch.setattr(tags, "_glibc_version_string_confstr", lambda: False) assert tags._glibc_version_string() == expected From 18885a92d146834c9e1fa91b9b75ac65116d07f6 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 30 Sep 2019 16:52:09 -0500 Subject: [PATCH 039/114] Update packaging/tags.py Co-Authored-By: Brett Cannon <54418+brettcannon@users.noreply.github.com> --- packaging/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/tags.py b/packaging/tags.py index bd03064f5..fb23995ab 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -347,7 +347,7 @@ def _glibc_version_string_confstr(): # 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": + # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17". version_string = os.confstr("CS_GNU_LIBC_VERSION") assert version_string is not None _, version = version_string.split() From 0dda3f0cd8e86a00bda60fe5997846fbffb4d5bb Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 30 Sep 2019 16:52:14 -0500 Subject: [PATCH 040/114] Update packaging/tags.py Co-Authored-By: Brett Cannon <54418+brettcannon@users.noreply.github.com> --- packaging/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/tags.py b/packaging/tags.py index fb23995ab..e5b739d71 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -344,7 +344,7 @@ def _glibc_version_string_confstr(): "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: + # 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". From 139b4c2c87cde2f89887abbace9b3a9fd8215f29 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 30 Sep 2019 16:52:21 -0500 Subject: [PATCH 041/114] Update packaging/tags.py Co-Authored-By: Brett Cannon <54418+brettcannon@users.noreply.github.com> --- packaging/tags.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packaging/tags.py b/packaging/tags.py index e5b739d71..813543441 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -360,7 +360,6 @@ def _glibc_version_string_confstr(): def _glibc_version_string_ctypes(): # type: () -> Optional[str] "Fallback implementation of glibc_version_string using ctypes." - try: import ctypes except ImportError: From 0da86bb6598f50c00c70294058023c9a1e52bdfc Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 30 Sep 2019 17:30:33 -0500 Subject: [PATCH 042/114] Default to no logging --- packaging/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/tags.py b/packaging/tags.py index 813543441..16e86602a 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -105,7 +105,7 @@ def parse_tag(tag): return frozenset(tags) -def _get_config_var(name, warn=True): +def _get_config_var(name, warn=False): # type: (str, Optional[bool]) -> Union[int, str, None] value = sysconfig.get_config_var(name) if value is None and warn: From c95460726f8642da20eff22f4f627744f499a7ca Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 1 Oct 2019 08:51:55 -0500 Subject: [PATCH 043/114] Add coverage for _get_config_var --- tests/test_tags.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_tags.py b/tests/test_tags.py index 59086fe79..b4f3923fb 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -556,6 +556,24 @@ def test_glibc_version_string_ctypes_missing(monkeypatch): assert tags._glibc_version_string_ctypes() is None +def test_get_config_var_does_not_log(monkeypatch): + debug = pretend.call_recorder(lambda *a: None) + monkeypatch.setattr(tags.logger, "debug", debug) + tags._get_config_var("missing") + assert debug.calls == [] + + +def test_get_config_var_does_log(monkeypatch): + debug = pretend.call_recorder(lambda *a: None) + monkeypatch.setattr(tags.logger, "debug", debug) + tags._get_config_var("missing", warn=True) + assert debug.calls == [ + pretend.call( + "Config variable '%s' is unset, Python ABI tag may be incorrect", "missing" + ) + ] + + def test_have_compatible_glibc(monkeypatch): if platform.system() == "Linux": # Assuming no one is running this test with a version of glibc released in From 6b2ede66fc33214c9b1e77b64f229f68925b6f4f Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Tue, 1 Oct 2019 19:39:11 -0500 Subject: [PATCH 044/114] Pass warn through from public API --- packaging/tags.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packaging/tags.py b/packaging/tags.py index 16e86602a..350fa1285 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -126,12 +126,12 @@ def _cpython_interpreter(py_version): return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) -def _cpython_abis(py_version): - # type: (PythonVersion) -> List[str] +def _cpython_abis(py_version, warn=False): + # type: (PythonVersion, Optional[bool]) -> List[str] abis = [] version = "{}{}".format(*py_version[:2]) debug = pymalloc = ucs4 = "" - with_debug = _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. @@ -140,11 +140,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 = _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 = _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 ): @@ -455,16 +455,16 @@ def _interpreter_name(): return INTERPRETER_SHORT_NAMES.get(name) or name -def _generic_interpreter(name, py_version): - # type: (str, PythonVersion) -> str - version = _get_config_var("py_version_nodot") +def _generic_interpreter(name, py_version, warn=False): + # type: (str, PythonVersion, Optional[bool]) -> str + version = _get_config_var("py_version_nodot", warn) if not version: version = "".join(map(str, py_version[:2])) return "{name}{version}".format(name=name, version=version) -def sys_tags(): - # type: () -> Iterator[Tag] +def sys_tags(warn=False): + # type: (Optional[bool]) -> Iterator[Tag] """ Returns the sequence of tag triples for the running interpreter. @@ -482,7 +482,7 @@ def sys_tags(): if interpreter_name == "cp": interpreter = _cpython_interpreter(py_version) - abis = _cpython_abis(py_version) + abis = _cpython_abis(py_version, warn) for tag in _cpython_tags(py_version, interpreter, abis, platforms): yield tag elif interpreter_name == "pp": @@ -491,7 +491,7 @@ def sys_tags(): for tag in _pypy_tags(py_version, interpreter, abi, platforms): yield tag else: - interpreter = _generic_interpreter(interpreter_name, py_version) + interpreter = _generic_interpreter(interpreter_name, py_version, warn) abi = _generic_abi() for tag in _generic_tags(interpreter, py_version, abi, platforms): yield tag From 3ccabc88d7ef6a1d9cfadb89a7926821315930e2 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 2 Oct 2019 00:00:56 -0500 Subject: [PATCH 045/114] Fix pypy tests --- tests/test_tags.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tags.py b/tests/test_tags.py index b4f3923fb..cd7f38e0e 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -326,7 +326,7 @@ def test_cpython_tags(): def test_sys_tags_on_mac_cpython(monkeypatch): if platform.python_implementation() != "CPython": monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") - monkeypatch.setattr(tags, "_cpython_abis", lambda py_version: ["cp33m"]) + monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) if platform.system() != "Darwin": monkeypatch.setattr(platform, "system", lambda: "Darwin") monkeypatch.setattr(tags, "_mac_platforms", lambda: ["macosx_10_5_x86_64"]) @@ -439,7 +439,7 @@ def test_generic_tags(): def test_sys_tags_on_windows_cpython(monkeypatch): if platform.python_implementation() != "CPython": monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") - monkeypatch.setattr(tags, "_cpython_abis", lambda py_version: ["cp33m"]) + monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) if platform.system() != "Windows": monkeypatch.setattr(platform, "system", lambda: "Windows") monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"]) @@ -652,7 +652,7 @@ def test_linux_platforms_manylinux2014(monkeypatch): def test_sys_tags_linux_cpython(monkeypatch): if platform.python_implementation() != "CPython": monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") - monkeypatch.setattr(tags, "_cpython_abis", lambda py_version: ["cp33m"]) + monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) if platform.system() != "Linux": monkeypatch.setattr(platform, "system", lambda: "Linux") monkeypatch.setattr(tags, "_linux_platforms", lambda: ["linux_x86_64"]) From 97dd2bd20173d3589e5ae387b19a66b56accba73 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 2 Oct 2019 00:11:23 -0500 Subject: [PATCH 046/114] Document the warn parameter --- docs/tags.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tags.rst b/docs/tags.rst index c6f70360f..cabe33b21 100644 --- a/docs/tags.rst +++ b/docs/tags.rst @@ -73,7 +73,7 @@ Reference :param str tag: The tag to parse, e.g. ``"py3-none-any"``. -.. function:: sys_tags() +.. function:: sys_tags(warn=False) Create an iterable of tags that the running interpreter supports. @@ -94,6 +94,8 @@ Reference short-circuiting of tag generation if the entire sequence is not necessary and calculating some tags happens to be expensive. + :param bool warn: Whether warnings should be logged. Defaults to ``False``. + .. _abbreviation codes: https://www.python.org/dev/peps/pep-0425/#python-tag .. _compressed tag set: https://www.python.org/dev/peps/pep-0425/#compressed-tag-sets From 1aaa1b02dd734251757cc7b882d05be80fa80dd0 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 3 Oct 2019 11:23:01 -0700 Subject: [PATCH 047/114] Add GitHub actions for everything covered by Tox (#209) --- .github/workflows/docs.yml | 27 +++++++++++++++++++++++++++ .github/workflows/lint.yml | 27 +++++++++++++++++++++++++++ .github/workflows/test.yml | 5 ++++- dev-requirements.txt | 5 +++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..d4e0d585e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,27 @@ +name: Documentation + +on: + push: + paths: + - 'docs/*' + +jobs: + docs: + name: tox -e docs + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-python@v1 + name: Install Python + with: + python-version: '3.7' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox + + - name: Build documentation + run: python -m tox -e docs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..18ee78397 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Linting + +on: + push: + paths: + - '*.py' + +jobs: + lint: + name: tox -e lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-python@v1 + name: Install Python + with: + python-version: '3.7' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox + + - name: Run `tox -e lint` + run: python -m tox -e lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca51ec5a2..b27ac3b02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,9 @@ name: Test -on: [push] +on: + push: + paths: + - '*.py' jobs: test: diff --git a/dev-requirements.txt b/dev-requirements.txt index 0b0442882..fcf187d49 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,10 +1,15 @@ # Install our development requirements black; python_version >= '3.6' +check-manifest coverage flake8 +mypy pep8-naming pretend pytest +readme_renderer +sphinx +sphinx_rtd_theme tox # Install packaging itself From 4d92b05e21142994e30ef245bcaa67df01964ef1 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 4 Oct 2019 18:23:00 -0700 Subject: [PATCH 048/114] Use sys.implementation.name if available PEP 440 prefers this over platform.python_implementation(). --- packaging/tags.py | 6 +++++- tests/test_tags.py | 44 ++++++++++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packaging/tags.py b/packaging/tags.py index 350fa1285..73e253147 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -451,7 +451,11 @@ def _generic_platforms(): def _interpreter_name(): # type: () -> str - name = platform.python_implementation().lower() + try: + name = sys.implementation.name + except AttributeError: # pragma: no cover + # Python 2.7 compatibility. + name = platform.python_implementation().lower() return INTERPRETER_SHORT_NAMES.get(name) or name diff --git a/tests/test_tags.py b/tests/test_tags.py index cd7f38e0e..19c55b448 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -39,6 +39,20 @@ def is_64bit_os(): return platform.architecture()[0] == "64bit" +@pytest.fixture +def mock_interpreter_name(monkeypatch): + def mock(name): + if hasattr(sys, "implementation") and sys.implementation.name != name.lower(): + monkeypatch.setattr(sys.implementation, "name", name.lower()) + return True + elif platform.python_implementation() != name: + monkeypatch.setattr(platform, "python_implementation", lambda: name) + return True + return False + + return mock + + def test_tag_lowercasing(): tag = tags.Tag("PY3", "None", "ANY") assert tag.interpreter == "py3" @@ -115,9 +129,8 @@ def test_parse_tag_multi_platform(): "name,expected", [("CPython", "cp"), ("PyPy", "pp"), ("Jython", "jy"), ("IronPython", "ip")], ) -def test__interpreter_name_cpython(name, expected, monkeypatch): - if platform.python_implementation().lower() != name: - monkeypatch.setattr(platform, "python_implementation", lambda: name) +def test__interpreter_name_cpython(name, expected, mock_interpreter_name): + mock_interpreter_name(name) assert tags._interpreter_name() == expected @@ -323,9 +336,8 @@ def test_cpython_tags(): ] -def test_sys_tags_on_mac_cpython(monkeypatch): - if platform.python_implementation() != "CPython": - monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") +def test_sys_tags_on_mac_cpython(mock_interpreter_name, monkeypatch): + if mock_interpreter_name("CPython"): monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) if platform.system() != "Darwin": monkeypatch.setattr(platform, "system", lambda: "Darwin") @@ -372,9 +384,8 @@ def test_pypy_interpreter(monkeypatch): assert expected == tags._pypy_interpreter() -def test_pypy_tags(monkeypatch): - if platform.python_implementation() != "PyPy": - monkeypatch.setattr(platform, "python_implementation", lambda: "PyPy") +def test_pypy_tags(mock_interpreter_name, monkeypatch): + if mock_interpreter_name("PyPy"): monkeypatch.setattr(tags, "_pypy_interpreter", lambda: "pp360") interpreter = tags._pypy_interpreter() result = list(tags._pypy_tags((3, 3), interpreter, "pypy3_60", ["plat1", "plat2"])) @@ -386,9 +397,8 @@ def test_pypy_tags(monkeypatch): ] -def test_sys_tags_on_mac_pypy(monkeypatch): - if platform.python_implementation() != "PyPy": - monkeypatch.setattr(platform, "python_implementation", lambda: "PyPy") +def test_sys_tags_on_mac_pypy(mock_interpreter_name, monkeypatch): + if mock_interpreter_name("PyPy"): monkeypatch.setattr(tags, "_pypy_interpreter", lambda: "pp360") if platform.system() != "Darwin": monkeypatch.setattr(platform, "system", lambda: "Darwin") @@ -436,9 +446,8 @@ def test_generic_tags(): ] -def test_sys_tags_on_windows_cpython(monkeypatch): - if platform.python_implementation() != "CPython": - monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") +def test_sys_tags_on_windows_cpython(mock_interpreter_name, monkeypatch): + if mock_interpreter_name("CPython"): monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) if platform.system() != "Windows": monkeypatch.setattr(platform, "system", lambda: "Windows") @@ -649,9 +658,8 @@ def test_linux_platforms_manylinux2014(monkeypatch): assert platforms == expected -def test_sys_tags_linux_cpython(monkeypatch): - if platform.python_implementation() != "CPython": - monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") +def test_sys_tags_linux_cpython(mock_interpreter_name, monkeypatch): + if mock_interpreter_name("CPython"): monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) if platform.system() != "Linux": monkeypatch.setattr(platform, "system", lambda: "Linux") From 82eae6cd2f872c90f7ae7a4d5970b1298f7edbe1 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 4 Oct 2019 18:23:25 -0700 Subject: [PATCH 049/114] Use twine to check package metadata Using setup.py for this is deprecated. --- tox.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9c692587b..1b3995cf2 100644 --- a/tox.ini +++ b/tox.ini @@ -24,9 +24,13 @@ basepython=python3 deps = pre-commit readme_renderer + setuptools + twine + wheel commands = pre-commit run --all-files - python setup.py check --metadata --restructuredtext --strict + python setup.py --quiet sdist bdist_wheel + twine check dist/* [testenv:docs] basepython = python3 From dadc0b65bad8fa362137e4bd9a5a54053a5425c4 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Mon, 7 Oct 2019 10:35:29 -0700 Subject: [PATCH 050/114] Trigger doc builds any time anything in docs/ is changed --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d4e0d585e..cafdec1df 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ name: Documentation on: push: paths: - - 'docs/*' + - 'docs/**' jobs: docs: From bc89a74bb8b2c090d6a345a738b62ed791deb798 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Mon, 7 Oct 2019 10:37:35 -0700 Subject: [PATCH 051/114] Fix lint action to run on any Python file changes --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 18ee78397..e7692d7b2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,7 +3,7 @@ name: Linting on: push: paths: - - '*.py' + - '**.py' jobs: lint: From 6d198194fbd2d91126a760038084bd1af398c3a2 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Mon, 7 Oct 2019 10:38:03 -0700 Subject: [PATCH 052/114] Fix the test action when any Python file is changed --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b27ac3b02..4fb0eeb73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: Test on: push: paths: - - '*.py' + - '**.py' jobs: test: From cafa1f39d8edd7844430505ec13b58f32525b8dd Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Wed, 9 Oct 2019 10:50:24 -0700 Subject: [PATCH 053/114] Delete dev-requirements.txt It isn't being kept up-to-date and people should be running tests locally via tox and/or pre-commit. Closes #214 --- dev-requirements.txt | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 dev-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index fcf187d49..000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Install our development requirements -black; python_version >= '3.6' -check-manifest -coverage -flake8 -mypy -pep8-naming -pretend -pytest -readme_renderer -sphinx -sphinx_rtd_theme -tox - -# Install packaging itself --e . From 11a48a17a759d560a271a10007a30652e2570944 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 9 Oct 2019 11:59:06 -0700 Subject: [PATCH 054/114] Archive build files for easy uploading later --- .github/workflows/lint.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e7692d7b2..b150cc28a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,3 +25,25 @@ jobs: - name: Run `tox -e lint` run: python -m tox -e lint + + build: + name: Build sdist and wheel + runs-on: ubuntu-latest + # Linting verifies that the project is in an acceptable state to create files + # for releasing. + # And this action should be run whenever a release is ready to go public as + # the version number will be changed by editing __about__.py. + needs: lint + + steps: + - name: Install dependencies + run: python -m pip install --upgrade setuptools wheel + + - name: Build + run: python setup.py sdist bdist_wheel + + - name: Archive files + uses: actions/upload-artifact@v1 + with: + name: dist + path: dist From c4f5fc23045f57a022d3a51dbd1c00de9056a2b9 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 10 Oct 2019 11:56:10 -0700 Subject: [PATCH 055/114] Update CHANGELOG --- CHANGELOG.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a42eb0a99..7ff2f26fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,14 @@ Changelog .. note:: This version is not yet released and is under active development. +* Add type hints (:issue:`191`) + +* Add proper trove classifiers for PyPy support (:issue:`198`) + +* Scale back depending on ``ctypes`` for manylinux support detection (:issue:`171`) + +* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (:issue:`193`) + 19.2 - 2019-09-18 ~~~~~~~~~~~~~~~~~ From f5e11285d1e3b02ef1114b0269a0e741cf1bdd41 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 10 Oct 2019 11:56:33 -0700 Subject: [PATCH 056/114] Hard-code the machine arch in some tests which are assuming x86_64 (#220) --- tests/test_tags.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/test_tags.py b/tests/test_tags.py index 19c55b448..c45ccd3a1 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -625,8 +625,7 @@ def test_linux_platforms_manylinux1(monkeypatch): if platform.system() != "Linux": monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") platforms = tags._linux_platforms(is_32bit=False) - arch = platform.machine() - assert platforms == ["manylinux1_" + arch, "linux_" + arch] + assert platforms == ["manylinux1_x86_64", "linux_x86_64"] def test_linux_platforms_manylinux2010(monkeypatch): @@ -636,8 +635,7 @@ def test_linux_platforms_manylinux2010(monkeypatch): if platform.system() != "Linux": monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") platforms = tags._linux_platforms(is_32bit=False) - arch = platform.machine() - expected = ["manylinux2010_" + arch, "manylinux1_" + arch, "linux_" + arch] + expected = ["manylinux2010_x86_64", "manylinux1_x86_64", "linux_x86_64"] assert platforms == expected @@ -648,12 +646,11 @@ def test_linux_platforms_manylinux2014(monkeypatch): if platform.system() != "Linux": monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") platforms = tags._linux_platforms(is_32bit=False) - arch = platform.machine() expected = [ - "manylinux2014_" + arch, - "manylinux2010_" + arch, - "manylinux1_" + arch, - "linux_" + arch, + "manylinux2014_x86_64", + "manylinux2010_x86_64", + "manylinux1_x86_64", + "linux_x86_64", ] assert platforms == expected From b1acd548e517c1b2fea6a95fd07bac3c2aee248c Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 10 Oct 2019 11:59:32 -0700 Subject: [PATCH 057/114] Specify dependencies for running tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4fb0eeb73..8d944191f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install -r dev-requirements.txt + python -m pip install pytest pretend - name: Test coverage run: | From cf733311227e515d6f12e621e6dc6da924f5bc19 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Thu, 10 Oct 2019 12:00:56 -0700 Subject: [PATCH 058/114] Fill in prerequisite steps in build job --- .github/workflows/lint.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b150cc28a..1b9660dc1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,6 +36,13 @@ jobs: needs: lint steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-python@v1 + name: Install Python + with: + python-version: '3.7' + - name: Install dependencies run: python -m pip install --upgrade setuptools wheel From 8fdc92330edc817cbf076a87e6b8ece2387ce9a3 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Fri, 18 Oct 2019 21:40:09 +0200 Subject: [PATCH 059/114] Fix linter issue introduced in cf733311227e515d6f12e621e6dc6da924f5bc19 (#222) --- .github/workflows/lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1b9660dc1..8b4054114 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -37,12 +37,12 @@ jobs: steps: - uses: actions/checkout@v1 - + - uses: actions/setup-python@v1 name: Install Python with: python-version: '3.7' - + - name: Install dependencies run: python -m pip install --upgrade setuptools wheel From f1b2b2222c8832a5a01f328afb46c9f898e67a1e Mon Sep 17 00:00:00 2001 From: Fridolin Pokorny Date: Wed, 23 Oct 2019 15:11:08 +0200 Subject: [PATCH 060/114] Introduce major, minor and micro aliases to Version --- packaging/version.py | 15 +++++++++++++++ tests/test_version.py | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packaging/version.py b/packaging/version.py index b1aff6558..f39a2a12a 100644 --- a/packaging/version.py +++ b/packaging/version.py @@ -401,6 +401,21 @@ 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 diff --git a/tests/test_version.py b/tests/test_version.py index 4e8079dce..67e18e9fe 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -758,6 +758,18 @@ def test_compare_legacyversion_version(self): result = sorted([Version("0"), LegacyVersion("1")]) assert result == [LegacyVersion("1"), Version("0")] + def test_major_version(self): + assert Version("2.1.0").major == 2 + + def test_minor_version(self): + assert Version("2.1.0").minor == 1 + assert Version("2").minor == 0 + + def test_micro_version(self): + assert Version("2.1.3").micro == 3 + assert Version("2.1").micro == 0 + assert Version("2").micro == 0 + LEGACY_VERSIONS = ["foobar", "a cat is fine too", "lolwut", "1-0", "2.0-a1"] From f04f4705b497cccc6a0a26463c9f6dbc5d7d3d9e Mon Sep 17 00:00:00 2001 From: Fridolin Pokorny Date: Sun, 27 Oct 2019 12:23:32 +0100 Subject: [PATCH 061/114] Package type information As per PEP-561, ship `py.typed' with packaging to allow type checking. --- packaging/py.typed | 0 setup.py | 1 + 2 files changed, 1 insertion(+) create mode 100644 packaging/py.typed diff --git a/packaging/py.typed b/packaging/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/setup.py b/setup.py index d6c6310f7..abe584aed 100644 --- a/setup.py +++ b/setup.py @@ -67,4 +67,5 @@ "Programming Language :: Python :: Implementation :: PyPy", ], packages=["packaging"], + package_data={"packaging": ["py.typed"]}, ) From e01e81739ceeabfc90802f4212ba27deafe83d0c Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 28 Oct 2019 14:11:30 -0500 Subject: [PATCH 062/114] Install coverage as dependency for test action --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d944191f..a5941a08b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install pytest pretend + python -m pip install coverage pytest pretend - name: Test coverage run: | From 3b598e8a01d3c0f54cf6067382cf3f2e113191d2 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 28 Oct 2019 14:15:21 -0500 Subject: [PATCH 063/114] Run workflows for PRs --- .github/workflows/docs.yml | 3 +++ .github/workflows/lint.yml | 3 +++ .github/workflows/test.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cafdec1df..af57c2fc2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,6 +1,9 @@ name: Documentation on: + pull_request: + paths: + - 'docs/**' push: paths: - 'docs/**' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8b4054114..576d5c39a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,9 @@ name: Linting on: + pull_request: + paths: + - '**.py' push: paths: - '**.py' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5941a08b..6e20dab73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,9 @@ name: Test on: + pull_request: + paths: + - '**.py' push: paths: - '**.py' From 19474035accf08ace51044c8529592d91a14f481 Mon Sep 17 00:00:00 2001 From: Christopher Hunt Date: Sun, 3 Nov 2019 22:37:19 -0500 Subject: [PATCH 064/114] Fix typo in comment in packaging.specifiers `indidivual` -> `individual` --- packaging/specifiers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index b366978e3..94987486d 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -651,7 +651,7 @@ class SpecifierSet(BaseSpecifier): def __init__(self, specifiers="", prereleases=None): # type: (str, Optional[bool]) -> None - # Split on , to break each indidivual specifier into it's own item, and + # Split on , to break each individual specifier into it's own item, and # strip each item to remove leading/trailing whitespace. split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] From 183b725079242253b54be8512d0dd9a5632886f4 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Fri, 15 Nov 2019 12:11:09 -0800 Subject: [PATCH 065/114] Add 'invoke' as a dependency --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e20dab73..8404ed711 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install coverage pytest pretend + python -m pip install coverage invoke pretend pytest - name: Test coverage run: | From cf57ddd66d7752de7682c867c6495b2df6c731fa Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Fri, 15 Nov 2019 12:17:46 -0800 Subject: [PATCH 066/114] Add missing 'invoke' dependency --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 1b3995cf2..6f9d82484 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = py27,pypy,pypy3,py34,py35,py36,py37,docs,lint [testenv] deps = coverage + invoke pretend pytest pip>=9.0.2 From 7028f5dc739d606bae00eff3bd3904a253bf4377 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Fri, 15 Nov 2019 12:27:50 -0800 Subject: [PATCH 067/114] Prevent pytest from searching the 'tasks' directory Otherwise it triggers an import error. --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 6f9d82484..6e8e87638 100644 --- a/tox.ini +++ b/tox.ini @@ -4,12 +4,11 @@ envlist = py27,pypy,pypy3,py34,py35,py36,py37,docs,lint [testenv] deps = coverage - invoke pretend pytest pip>=9.0.2 commands = - python -m coverage run --source packaging/ -m pytest --strict {posargs} + python -m coverage run --source packaging/ -m pytest --strict {posargs} tests python -m coverage report -m --fail-under 100 [testenv:pypy] From 2822486760476d4b223ea7720e03cd9b714203d1 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Fri, 15 Nov 2019 12:28:45 -0800 Subject: [PATCH 068/114] Have pytest only search in 'tests' Otherwise it tries to import everything in 'tasks'. --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8404ed711..891aac6bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,9 +30,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install coverage invoke pretend pytest + python -m pip install coverage pretend pytest - name: Test coverage run: | - python -m coverage run --source packaging/ -m pytest --strict + python -m coverage run --source packaging/ -m pytest --strict tests python -m coverage report -m --fail-under 100 From 4389d1b701478a1e0a8cc8268fbf5684092c7f49 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Fri, 15 Nov 2019 13:27:43 -0800 Subject: [PATCH 069/114] Make pytest happy under PyPy --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 6e8e87638..c3e6273ff 100644 --- a/tox.ini +++ b/tox.ini @@ -13,11 +13,11 @@ commands = [testenv:pypy] commands = - pytest --capture=no --strict {posargs} + pytest --capture=no --strict {posargs} tests [testenv:pypy3] commands = - pytest --capture=no --strict {posargs} + pytest --capture=no --strict {posargs} tests [testenv:lint] basepython=python3 From bdac09ead2b9be7db3b84a024c6d0c537faea62f Mon Sep 17 00:00:00 2001 From: Cristina Date: Fri, 15 Nov 2019 15:56:33 -0800 Subject: [PATCH 070/114] Add support for Python 3.8 (#236) Fixes #232. Also, make 3.8 the default for github actions. --- .github/workflows/docs.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/test.yml | 2 +- setup.py | 1 + tox.ini | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index af57c2fc2..e8c579536 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-python@v1 name: Install Python with: - python-version: '3.7' + python-version: '3.8' - name: Install dependencies run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 576d5c39a..8dd3217cc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-python@v1 name: Install Python with: - python-version: '3.7' + python-version: '3.8' - name: Install dependencies run: | @@ -44,7 +44,7 @@ jobs: - uses: actions/setup-python@v1 name: Install Python with: - python-version: '3.7' + python-version: '3.8' - name: Install dependencies run: python -m pip install --upgrade setuptools wheel diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 891aac6bf..4adcd1af6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] # Python 3.4 is not available from actions/setup-python@v1. - python_version: ['2.7', '3.5', '3.6', '3.7'] + python_version: ['2.7', '3.5', '3.6', '3.7', '3.8'] steps: - uses: actions/checkout@v1 diff --git a/setup.py b/setup.py index abe584aed..f533a7d4a 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], diff --git a/tox.ini b/tox.ini index c3e6273ff..68c011f00 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,pypy,pypy3,py34,py35,py36,py37,docs,lint +envlist = py27,pypy,pypy3,py34,py35,py36,py37,py38,docs,lint [testenv] deps = From f92c8eb22dac0c893bc43ac9099dabd865cf3bcd Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Sat, 23 Nov 2019 12:56:35 -0800 Subject: [PATCH 071/114] Document use of pre-commit (#241) --- docs/development/getting-started.rst | 21 ++++++++------------- docs/development/submitting-patches.rst | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/docs/development/getting-started.rst b/docs/development/getting-started.rst index aa47fe684..75a1d8014 100644 --- a/docs/development/getting-started.rst +++ b/docs/development/getting-started.rst @@ -2,22 +2,13 @@ Getting started =============== Working on packaging requires the installation of a small number of -development dependencies. These are listed in ``dev-requirements.txt`` and they -can be installed in a `virtualenv`_ using `pip`_. Once you've installed the -dependencies, install packaging in ``editable`` mode. For example: - -.. code-block:: console - - $ # Create a virtualenv and activate it - $ python -m pip install --requirement dev-requirements.txt - $ python -m pip install --editable . - -You are now ready to run the tests and build the documentation. +development dependencies. To see what dependencies are required to +run the tests manually, please look at the ``tox.ini`` file. Running tests ~~~~~~~~~~~~~ -packaging unit tests are found in the ``tests/`` directory and are +The packaging unit tests are found in the ``tests/`` directory and are designed to be run using `pytest`_. `pytest`_ will discover the tests automatically, so all you have to do is: @@ -27,7 +18,8 @@ automatically, so all you have to do is: ... 62746 passed in 220.43 seconds -This runs the tests with the default Python interpreter. +This runs the tests with the default Python interpreter. This also allows +you to run select tests instead of the entire test suite. You can also verify that the tests pass on other supported Python interpreters. For this we use `tox`_, which will automatically create a `virtualenv`_ for @@ -49,6 +41,8 @@ each supported Python version and run the tests. For example: You may not have all the required Python versions installed, in which case you will see one or more ``InterpreterNotFound`` errors. +If you wish to run just the linting rules, you may use `pre-commit`_. + Building documentation ~~~~~~~~~~~~~~~~~~~~~~ @@ -74,3 +68,4 @@ The HTML documentation index can now be found at .. _`pip`: https://pypi.org/project/pip/ .. _`sphinx`: https://pypi.org/project/Sphinx/ .. _`reStructured Text`: http://sphinx-doc.org/rest.html +.. _`pre-commit`: https://pre-commit.com diff --git a/docs/development/submitting-patches.rst b/docs/development/submitting-patches.rst index 875b79030..f139c778b 100644 --- a/docs/development/submitting-patches.rst +++ b/docs/development/submitting-patches.rst @@ -20,7 +20,7 @@ Code This project's source is auto-formatted with |black|. You can check if your code meets our requirements by running our linters against it with ``tox -e -lint``. +lint`` or ``pre-commit run --all-files``. `Write comments as complete sentences.`_ From 41b0ad532bc3520cf0b800de1e94e1bfa94fe0a3 Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Mon, 25 Nov 2019 10:59:53 -0800 Subject: [PATCH 072/114] Expose various function in `tags` publicly (#231) --- .pre-commit-config.yaml | 2 +- docs/tags.rst | 111 +++- packaging/tags.py | 326 ++++++---- tests/test_tags.py | 1321 ++++++++++++++++++++++----------------- 4 files changed, 1054 insertions(+), 706 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 367e62e4f..936a21233 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.720 + rev: v0.740 hooks: - id: mypy exclude: '^(docs|tasks|tests)|setup\.py' diff --git a/docs/tags.rst b/docs/tags.rst index cabe33b21..0851d7893 100644 --- a/docs/tags.rst +++ b/docs/tags.rst @@ -38,6 +38,7 @@ Reference A dictionary mapping interpreter names to their `abbreviation codes`_ (e.g. ``"cpython"`` is ``"cp"``). All interpreter names are lower-case. + .. class:: Tag(interpreter, abi, platform) A representation of the tag triple for a wheel. Instances are considered @@ -65,17 +66,18 @@ Reference .. function:: parse_tag(tag) - Parse the provided *tag* into a set of :class:`Tag` instances. + Parses the provided ``tag`` into a set of :class:`Tag` instances. - The returning of a set is required due to the possibility that the tag is a - `compressed tag set`_, e.g. ``"py2.py3-none-any"``. + Returning a set is required due to the possibility that the tag is a + `compressed tag set`_, e.g. ``"py2.py3-none-any"`` which supports both + Python 2 and Python 3. :param str tag: The tag to parse, e.g. ``"py3-none-any"``. -.. function:: sys_tags(warn=False) +.. function:: sys_tags(*, warn=False) - Create an iterable of tags that the running interpreter supports. + Yields the tags that the running interpreter supports. The iterable is ordered so that the best-matching tag is first in the sequence. The exact preferential order to tags is interpreter-specific, but @@ -92,11 +94,108 @@ Reference The function returns an iterable in order to allow for the possible short-circuiting of tag generation if the entire sequence is not necessary - and calculating some tags happens to be expensive. + and tag calculation happens to be expensive. :param bool warn: Whether warnings should be logged. Defaults to ``False``. +.. function:: interpreter_name() + + Returns the running interpreter's name. + + This typically acts as the prefix to the :attr:`~Tag.interpreter` tag. + + +.. function:: interpreter_version(*, warn=False) + + Returns the running interpreter's version. + + This typically acts as the suffix to the :attr:`~Tag.interpreter` tag. + + +.. function:: mac_platforms(version=None, arch=None) + + Yields the :attr:`~Tag.platform` tags for macOS. + + :param tuple version: A two-item tuple presenting the version of macOS. + Defaults to the current system's version. + :param str arch: The CPU architecture. Defaults to the architecture of the + current system, e.g. ``"x86_64"``. + + .. note:: + Equivalent support for the other major platforms is purposefully not + provided: + + - On Windows, platform compatibility is statically specified + - On Linux, code must be run on the system itself to determine + compatibility + + +.. function:: compatible_tags(python_version=None, interpreter=None, platforms=None) + + Yields the tags for an interpreter compatible with the Python version + specified by ``python_version``. + + The specific tags generated are: + + - ``py*-none-`` + - ``-none-any`` if ``interpreter`` is provided + - ``py*-none-any`` + + :param Sequence python_version: A one- or two-item sequence representing the + compatible version of Python. Defaults to + ``sys.version_info[:2]``. + :param str interpreter: The name of the interpreter (if known), e.g. + ``"cp38"``. Defaults to the current interpreter. + :param Iterable platforms: Iterable of compatible platforms. Defaults to the + platforms compatible with the current system. + +.. function:: cpython_tags(python_version=None, abis=None, platforms=None, *, warn=False) + + Yields the tags for the CPython interpreter. + + The specific tags generated are: + + - ``cp--`` + - ``cp-abi3-`` + - ``cp-none-`` + - ``cp-abi3-`` where "older version" is all older + minor versions down to Python 3.2 (when ``abi3`` was introduced) + + If ``python_version`` only provides a major-only version then only + user-provided ABIs via ``abis`` and the ``none`` ABI will be used. + + :param Sequence python_version: A one- or two-item sequence representing the + targetted Python version. Defaults to + ``sys.version_info[:2]``. + :param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs + compatible with the current system. + :param Iterable platforms: Iterable of compatible platforms. Defaults to the + platforms compatible with the current system. + :param bool warn: Whether warnings should be logged. Defaults to ``False``. + +.. function:: generic_tags(interpreter=None, abis=None, platforms=None, *, warn=False) + + Yields the tags for an interpreter which requires no specialization. + + This function should be used if one of the other interpreter-specific + functions provided by this module is not appropriate (i.e. not calculating + tags for a CPython interpreter). + + The specific tags generated are: + + - ``--`` + + The ``"none"`` ABI will be added if it was not explicitly provided. + + :param str interpreter: The name of the interpreter. Defaults to being + calculated. + :param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs + compatible with the current system. + :param Iterable platforms: Iterable of compatible platforms. Defaults to the + platforms compatible with the current system. + :param bool warn: Whether warnings should be logged. Defaults to ``False``. + .. _abbreviation codes: https://www.python.org/dev/peps/pep-0425/#python-tag .. _compressed tag set: https://www.python.org/dev/peps/pep-0425/#compressed-tag-sets .. _platform compatibility tags: https://packaging.python.org/specifications/platform-compatibility-tags/ diff --git a/packaging/tags.py b/packaging/tags.py index 73e253147..a719d4e73 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -24,9 +24,19 @@ from ._typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: # pragma: no cover - from typing import Dict, FrozenSet, Iterable, Iterator, List, Optional, Tuple, Union + from typing import ( + Dict, + FrozenSet, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, + ) - PythonVersion = Tuple[int, int] + PythonVersion = Sequence[int] MacVersion = Tuple[int, int] GlibcVersion = Tuple[int, int] @@ -105,8 +115,24 @@ 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, Optional[bool]) -> Union[int, str, None] + # type: (str, bool) -> Union[int, str, None] value = sysconfig.get_config_var(name) if value is None and warn: logger.debug( @@ -120,14 +146,9 @@ def _normalize_string(string): return string.replace(".", "_").replace("-", "_") -def _cpython_interpreter(py_version): - # type: (PythonVersion) -> str - # TODO: Is using py_version_nodot for interpreter version critical? - return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) - - def _cpython_abis(py_version, warn=False): - # type: (PythonVersion, Optional[bool]) -> List[str] + # type: (PythonVersion, bool) -> List[str] + py_version = tuple(py_version) # To allow for version comparison. abis = [] version = "{}{}".format(*py_version[:2]) debug = pymalloc = ucs4 = "" @@ -162,91 +183,150 @@ def _cpython_abis(py_version, warn=False): return abis -def _cpython_tags(py_version, interpreter, abis, platforms): - # type: (PythonVersion, str, Iterable[str], Iterable[str]) -> Iterator[Tag] +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-- + - cp-abi3- + - cp-none- + - cp-abi3- # 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] + + if len(python_version) < 2: + interpreter = "cp{}".format(python_version[0]) + else: + interpreter = "cp{}{}".format(*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 + # Not worrying about the case of Python 3.2 or older being specified and + # thus having redundant tags thanks to the abi3 in-fill later on as + # 'packaging' doesn't directly support Python that far back. + if len(python_version) > 1: + 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(): - # type: () -> str - # Ignoring sys.pypy_version_info for type checking due to typeshed lacking - # the reference to the attribute. - return "pp{py_major}{pypy_major}{pypy_minor}".format( - py_major=sys.version_info[0], - pypy_major=sys.pypy_version_info.major, # type: ignore - pypy_minor=sys.pypy_version_info.minor, # type: ignore - ) + if len(python_version) > 1: + for minor_version in range(python_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{major}{minor}".format( + major=python_version[0], minor=minor_version + ) + yield Tag(interpreter, "abi3", platform_) def _generic_abi(): - # type: () -> str + # 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): - # type: (PythonVersion, str, str, Iterable[str]) -> Iterator[Tag] - 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: + - -- -def _generic_tags(interpreter, py_version, abi, platforms): - # type: (str, PythonVersion, str, Iterable[str]) -> Iterator[Tag] - 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{major}{minor}".format(major=py_version[0], minor=py_version[1]) 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{major}{minor}".format(major=py_version[0], minor=minor) -def _independent_tags(interpreter, py_version, platforms): - # type: (str, PythonVersion, Iterable[str]) -> Iterator[Tag] +def compatible_tags( + python_version=None, # type: Optional[PythonVersion] + interpreter=None, # type: Optional[str] + platforms=None, # type: Optional[Iterator[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- - - -none-any + - -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] + if not platforms: + platforms = _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") @@ -289,11 +369,16 @@ def _mac_binary_formats(version, cpu_arch): return formats -def _mac_platforms( - version=None, # type: Optional[MacVersion] - arch=None, # type: Optional[str] -): - # type: (...) -> List[str] +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 = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) @@ -303,19 +388,15 @@ def _mac_platforms( arch = _mac_arch(cpu_arch) else: arch = arch - platforms = [] 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. @@ -323,7 +404,7 @@ 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): @@ -341,7 +422,9 @@ def _glibc_version_string(): def _glibc_version_string_confstr(): # type: () -> Optional[str] - "Primary implementation of glibc_version_string using os.confstr." + """ + 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. @@ -359,7 +442,9 @@ def _glibc_version_string_confstr(): def _glibc_version_string_ctypes(): # type: () -> Optional[str] - "Fallback implementation of glibc_version_string using ctypes." + """ + Fallback implementation of glibc_version_string using ctypes. + """ try: import ctypes except ImportError: @@ -421,7 +506,7 @@ def _have_compatible_glibc(required_major, minimum_minor): def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): - # type: (bool) -> List[str] + # type: (bool) -> Iterator[str] linux = _normalize_string(distutils.util.get_platform()) if linux == "linux_x86_64" and is_32bit: linux = "linux_i686" @@ -433,71 +518,76 @@ def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): 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(): - # type: () -> List[str] - platform = _normalize_string(distutils.util.get_platform()) - return [platform] + # type: () -> Iterator[str] + yield _normalize_string(distutils.util.get_platform()) + +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(): + +def interpreter_name(): # type: () -> str + """ + Returns the name of the running interpreter. + """ try: - name = sys.implementation.name + 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, warn=False): - # type: (str, PythonVersion, Optional[bool]) -> str - version = _get_config_var("py_version_nodot", warn) - 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 = "".join(map(str, sys.version_info[:2])) + return version -def sys_tags(warn=False): - # type: (Optional[bool]) -> Iterator[Tag] +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, warn) - 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, warn) - 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 diff --git a/tests/test_tags.py b/tests/test_tags.py index c45ccd3a1..0a89991e1 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -2,7 +2,10 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -import collections +try: + import collections.abc as collections_abc +except ImportError: + import collections as collections_abc try: import ctypes @@ -39,6 +42,15 @@ def is_64bit_os(): return platform.architecture()[0] == "64bit" +@pytest.fixture +def manylinux_module(monkeypatch): + monkeypatch.setattr(tags, "_have_compatible_glibc", lambda *args: False) + module_name = "_manylinux" + module = types.ModuleType(module_name) + monkeypatch.setitem(sys.modules, module_name, module) + return module + + @pytest.fixture def mock_interpreter_name(monkeypatch): def mock(name): @@ -53,630 +65,777 @@ def mock(name): return mock -def test_tag_lowercasing(): - tag = tags.Tag("PY3", "None", "ANY") - assert tag.interpreter == "py3" - assert tag.abi == "none" - assert tag.platform == "any" - - -def test_tag_equality(): - args = "py3", "none", "any" - assert tags.Tag(*args) == tags.Tag(*args) - - -def test_tag_equality_fails_with_non_tag(): - assert not tags.Tag("py3", "none", "any") == "non-tag" - - -def test_tag_hashing(example_tag): - tags = {example_tag} # Should not raise TypeError. - assert example_tag in tags - - -def test_tag_hash_equality(example_tag): - equal_tag = tags.Tag("py3", "none", "any") - assert example_tag == equal_tag - assert example_tag.__hash__() == equal_tag.__hash__() - +class TestTag: + def test_lowercasing(self): + tag = tags.Tag("PY3", "None", "ANY") + assert tag.interpreter == "py3" + assert tag.abi == "none" + assert tag.platform == "any" -def test_tag_str(example_tag): - assert str(example_tag) == "py3-none-any" + def test_equality(self): + args = "py3", "none", "any" + assert tags.Tag(*args) == tags.Tag(*args) + def test_equality_fails_with_non_tag(self): + assert not tags.Tag("py3", "none", "any") == "non-tag" -def test_tag_repr(example_tag): - assert repr(example_tag) == "".format( - tag_id=id(example_tag) - ) - - -def test_tag_attribute_access(example_tag): - assert example_tag.interpreter == "py3" - assert example_tag.abi == "none" - assert example_tag.platform == "any" - - -def test_parse_tag_simple(example_tag): - parsed_tags = tags.parse_tag(str(example_tag)) - assert parsed_tags == {example_tag} + def test_hashing(self, example_tag): + tags = {example_tag} # Should not raise TypeError. + assert example_tag in tags + def test_hash_equality(self, example_tag): + equal_tag = tags.Tag("py3", "none", "any") + assert example_tag == equal_tag # Sanity check. + assert example_tag.__hash__() == equal_tag.__hash__() -def test_parse_tag_multi_interpreter(example_tag): - expected = {example_tag, tags.Tag("py2", "none", "any")} - given = tags.parse_tag("py2.py3-none-any") - assert given == expected + def test_str(self, example_tag): + assert str(example_tag) == "py3-none-any" - -def test_parse_tag_multi_platform(): - expected = { - tags.Tag("cp37", "cp37m", platform) - for platform in ( - "macosx_10_6_intel", - "macosx_10_9_intel", - "macosx_10_9_x86_64", - "macosx_10_10_intel", - "macosx_10_10_x86_64", + def test_repr(self, example_tag): + assert repr(example_tag) == "".format( + tag_id=id(example_tag) ) - } - given = tags.parse_tag( - "cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64." - "macosx_10_10_intel.macosx_10_10_x86_64" - ) - assert given == expected - - -@pytest.mark.parametrize( - "name,expected", - [("CPython", "cp"), ("PyPy", "pp"), ("Jython", "jy"), ("IronPython", "ip")], -) -def test__interpreter_name_cpython(name, expected, mock_interpreter_name): - mock_interpreter_name(name) - assert tags._interpreter_name() == expected + def test_attribute_access(self, example_tag): + assert example_tag.interpreter == "py3" + assert example_tag.abi == "none" + assert example_tag.platform == "any" -@pytest.mark.parametrize( - "arch, is_32bit, expected", - [ - ("i386", True, "i386"), - ("ppc", True, "ppc"), - ("x86_64", False, "x86_64"), - ("x86_64", True, "i386"), - ("ppc64", False, "ppc64"), - ("ppc64", True, "ppc"), - ], -) -def test_macos_architectures(arch, is_32bit, expected): - assert tags._mac_arch(arch, is_32bit=is_32bit) == expected +class TestWarnKeywordOnlyParameter: + def test_no_argument(self): + assert not tags._warn_keyword_parameter("test_warn_keyword_parameters", {}) -@pytest.mark.parametrize( - "version,arch,expected", - [ - ((10, 17), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), - ((10, 4), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), - ((10, 3), "x86_64", []), - ((10, 17), "i386", ["i386", "intel", "fat32", "fat", "universal"]), - ((10, 4), "i386", ["i386", "intel", "fat32", "fat", "universal"]), - ((10, 3), "i386", []), - ((10, 17), "ppc64", []), - ((10, 6), "ppc64", []), - ((10, 5), "ppc64", ["ppc64", "fat64", "universal"]), - ((10, 3), "ppc64", []), - ((10, 17), "ppc", []), - ((10, 7), "ppc", []), - ((10, 6), "ppc", ["ppc", "fat32", "fat", "universal"]), - ((10, 0), "ppc", ["ppc", "fat32", "fat", "universal"]), - ((11, 0), "riscv", ["riscv", "universal"]), - ], -) -def test_macos_binary_formats(version, arch, expected): - assert tags._mac_binary_formats(version, arch) == expected - - -def test_mac_platforms(): - platforms = tags._mac_platforms((10, 5), "x86_64") - assert platforms == [ - "macosx_10_5_x86_64", - "macosx_10_5_intel", - "macosx_10_5_fat64", - "macosx_10_5_fat32", - "macosx_10_5_universal", - "macosx_10_4_x86_64", - "macosx_10_4_intel", - "macosx_10_4_fat64", - "macosx_10_4_fat32", - "macosx_10_4_universal", - ] - - assert len(tags._mac_platforms((10, 17), "x86_64")) == 14 * 5 - - assert not tags._mac_platforms((10, 0), "x86_64") - - -def test_macos_version_detection(monkeypatch): - if platform.system() != "Darwin": - monkeypatch.setattr( - platform, "mac_ver", lambda: ("10.14", ("", "", ""), "x86_64") + def test_false(self): + assert not tags._warn_keyword_parameter( + "test_warn_keyword_parameters", {"warn": False} ) - version = platform.mac_ver()[0].split(".") - expected = "macosx_{major}_{minor}".format(major=version[0], minor=version[1]) - platforms = tags._mac_platforms(arch="x86_64") - assert platforms[0].startswith(expected) - - -@pytest.mark.parametrize("arch", ["x86_64", "i386"]) -def test_macos_arch_detection(arch, monkeypatch): - if platform.system() != "Darwin" or platform.mac_ver()[2] != arch: - monkeypatch.setattr(platform, "mac_ver", lambda: ("10.14", ("", "", ""), arch)) - monkeypatch.setattr(tags, "_mac_arch", lambda *args: arch) - assert tags._mac_platforms((10, 14))[0].endswith(arch) - - -@pytest.mark.parametrize( - "py_debug,gettotalrefcount,result", - [(1, False, True), (0, False, False), (None, True, True)], -) -def test_cpython_abis_debug(py_debug, gettotalrefcount, result, monkeypatch): - config = {"Py_DEBUG": py_debug, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": 2} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - if gettotalrefcount: - monkeypatch.setattr(sys, "gettotalrefcount", 1, raising=False) - expected = ["cp37d" if result else "cp37"] - assert tags._cpython_abis((3, 7)) == expected - - -def test_cpython_abis_debug_file_extension(monkeypatch): - config = {"Py_DEBUG": None} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - monkeypatch.delattr(sys, "gettotalrefcount", raising=False) - monkeypatch.setattr(tags, "EXTENSION_SUFFIXES", {"_d.pyd"}) - assert tags._cpython_abis((3, 8)) == ["cp38d", "cp38"] - - -@pytest.mark.parametrize( - "debug,expected", [(True, ["cp38d", "cp38"]), (False, ["cp38"])] -) -def test_cpython_abis_debug_38(debug, expected, monkeypatch): - config = {"Py_DEBUG": debug} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - assert tags._cpython_abis((3, 8)) == expected - - -@pytest.mark.parametrize( - "pymalloc,version,result", - [(1, (3, 7), True), (0, (3, 7), False), (None, (3, 7), True), (1, (3, 8), False)], -) -def test_cpython_abis_pymalloc(pymalloc, version, result, monkeypatch): - config = {"Py_DEBUG": 0, "WITH_PYMALLOC": pymalloc, "Py_UNICODE_SIZE": 2} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - base_abi = "cp{}{}".format(version[0], version[1]) - expected = [base_abi + "m" if result else base_abi] - assert tags._cpython_abis(version) == expected + def test_true(self): + assert tags._warn_keyword_parameter( + "test_warn_keyword_parameters", {"warn": True} + ) -@pytest.mark.parametrize( - "unicode_size,maxunicode,version,result", - [ - (4, 0x10FFFF, (3, 2), True), - (2, 0xFFFF, (3, 2), False), - (None, 0x10FFFF, (3, 2), True), - (None, 0xFFFF, (3, 2), False), - (4, 0x10FFFF, (3, 3), False), - ], -) -def test_cpython_abis_wide_unicode( - unicode_size, maxunicode, version, result, monkeypatch -): - config = {"Py_DEBUG": 0, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": unicode_size} - monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) - monkeypatch.setattr(sys, "maxunicode", maxunicode) - base_abi = "cp{}{}".format(version[0], version[1]) - expected = [base_abi + "u" if result else base_abi] - assert tags._cpython_abis(version) == expected - - -def test_independent_tags(): - result = list(tags._independent_tags("cp33", (3, 3), ["plat1", "plat2"])) - assert result == [ - tags.Tag("py33", "none", "plat1"), - tags.Tag("py33", "none", "plat2"), - tags.Tag("py3", "none", "plat1"), - tags.Tag("py3", "none", "plat2"), - tags.Tag("py32", "none", "plat1"), - tags.Tag("py32", "none", "plat2"), - tags.Tag("py31", "none", "plat1"), - tags.Tag("py31", "none", "plat2"), - tags.Tag("py30", "none", "plat1"), - tags.Tag("py30", "none", "plat2"), - tags.Tag("cp33", "none", "any"), - tags.Tag("py33", "none", "any"), - tags.Tag("py3", "none", "any"), - tags.Tag("py32", "none", "any"), - tags.Tag("py31", "none", "any"), - tags.Tag("py30", "none", "any"), - ] - - -def test_cpython_tags(): - result = list( - tags._cpython_tags((3, 8), "cp38", ["cp38d", "cp38"], ["plat1", "plat2"]) + def test_too_many_arguments(self): + message_re = re.compile(r"too_many.+{!r}".format("whatever")) + with pytest.raises(TypeError, match=message_re): + tags._warn_keyword_parameter("too_many", {"warn": True, "whatever": True}) + + def test_wrong_argument(self): + message_re = re.compile(r"missing.+{!r}".format("unexpected")) + with pytest.raises(TypeError, match=message_re): + tags._warn_keyword_parameter("missing", {"unexpected": True}) + + +class TestParseTag: + def test_simple(self, example_tag): + parsed_tags = tags.parse_tag(str(example_tag)) + assert parsed_tags == {example_tag} + + def test_multi_interpreter(self, example_tag): + expected = {example_tag, tags.Tag("py2", "none", "any")} + given = tags.parse_tag("py2.py3-none-any") + assert given == expected + + def test_multi_platform(self): + expected = { + tags.Tag("cp37", "cp37m", platform) + for platform in ( + "macosx_10_6_intel", + "macosx_10_9_intel", + "macosx_10_9_x86_64", + "macosx_10_10_intel", + "macosx_10_10_x86_64", + ) + } + given = tags.parse_tag( + "cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64." + "macosx_10_10_intel.macosx_10_10_x86_64" + ) + assert given == expected + + +class TestInterpreterName: + def test_sys_implementation_name(self, monkeypatch): + class MockImplementation(object): + pass + + mock_implementation = MockImplementation() + mock_implementation.name = "sillywalk" + monkeypatch.setattr(sys, "implementation", mock_implementation, raising=False) + assert tags.interpreter_name() == "sillywalk" + + def test_platform(self, monkeypatch): + monkeypatch.delattr(sys, "implementation", raising=False) + name = "SillyWalk" + monkeypatch.setattr(platform, "python_implementation", lambda: name) + assert tags.interpreter_name() == name.lower() + + def test_interpreter_short_names(self, mock_interpreter_name, monkeypatch): + mock_interpreter_name("cpython") + assert tags.interpreter_name() == "cp" + + +class TestInterpreterVersion: + def test_warn(self, monkeypatch): + class MockConfigVar(object): + def __init__(self, return_): + self.warn = None + self._return = return_ + + def __call__(self, name, warn): + self.warn = warn + return self._return + + mock_config_var = MockConfigVar("38") + monkeypatch.setattr(tags, "_get_config_var", mock_config_var) + tags.interpreter_version(warn=True) + assert mock_config_var.warn + + def test_python_version_nodot(self, monkeypatch): + monkeypatch.setattr(tags, "_get_config_var", lambda var, warn: "NN") + assert tags.interpreter_version() == "NN" + + def test_sys_version_info(self, monkeypatch): + monkeypatch.setattr(tags, "_get_config_var", lambda *args, **kwargs: None) + monkeypatch.setattr(sys, "version_info", ("L", "M", "N")) + assert tags.interpreter_version() == "LM" + + +class TestMacOSPlatforms: + @pytest.mark.parametrize( + "arch, is_32bit, expected", + [ + ("i386", True, "i386"), + ("ppc", True, "ppc"), + ("x86_64", False, "x86_64"), + ("x86_64", True, "i386"), + ("ppc64", False, "ppc64"), + ("ppc64", True, "ppc"), + ], ) - assert result == [ - tags.Tag("cp38", "cp38d", "plat1"), - tags.Tag("cp38", "cp38d", "plat2"), - tags.Tag("cp38", "cp38", "plat1"), - tags.Tag("cp38", "cp38", "plat2"), - tags.Tag("cp38", "abi3", "plat1"), - tags.Tag("cp38", "abi3", "plat2"), - tags.Tag("cp38", "none", "plat1"), - tags.Tag("cp38", "none", "plat2"), - tags.Tag("cp37", "abi3", "plat1"), - tags.Tag("cp37", "abi3", "plat2"), - tags.Tag("cp36", "abi3", "plat1"), - tags.Tag("cp36", "abi3", "plat2"), - tags.Tag("cp35", "abi3", "plat1"), - tags.Tag("cp35", "abi3", "plat2"), - tags.Tag("cp34", "abi3", "plat1"), - tags.Tag("cp34", "abi3", "plat2"), - tags.Tag("cp33", "abi3", "plat1"), - tags.Tag("cp33", "abi3", "plat2"), - tags.Tag("cp32", "abi3", "plat1"), - tags.Tag("cp32", "abi3", "plat2"), - ] - result = list(tags._cpython_tags((3, 3), "cp33", ["cp33m"], ["plat1", "plat2"])) - assert result == [ - tags.Tag("cp33", "cp33m", "plat1"), - tags.Tag("cp33", "cp33m", "plat2"), - tags.Tag("cp33", "abi3", "plat1"), - tags.Tag("cp33", "abi3", "plat2"), - tags.Tag("cp33", "none", "plat1"), - tags.Tag("cp33", "none", "plat2"), - tags.Tag("cp32", "abi3", "plat1"), - tags.Tag("cp32", "abi3", "plat2"), - ] - - -def test_sys_tags_on_mac_cpython(mock_interpreter_name, monkeypatch): - if mock_interpreter_name("CPython"): - monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) - if platform.system() != "Darwin": - monkeypatch.setattr(platform, "system", lambda: "Darwin") - monkeypatch.setattr(tags, "_mac_platforms", lambda: ["macosx_10_5_x86_64"]) - abis = tags._cpython_abis(sys.version_info[:2]) - platforms = tags._mac_platforms() - result = list(tags.sys_tags()) - assert len(abis) == 1 - assert result[0] == tags.Tag( - "cp{major}{minor}".format(major=sys.version_info[0], minor=sys.version_info[1]), - abis[0], - platforms[0], + def test_architectures(self, arch, is_32bit, expected): + assert tags._mac_arch(arch, is_32bit=is_32bit) == expected + + @pytest.mark.parametrize( + "version,arch,expected", + [ + ((10, 17), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), + ((10, 4), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), + ((10, 3), "x86_64", []), + ((10, 17), "i386", ["i386", "intel", "fat32", "fat", "universal"]), + ((10, 4), "i386", ["i386", "intel", "fat32", "fat", "universal"]), + ((10, 3), "i386", []), + ((10, 17), "ppc64", []), + ((10, 6), "ppc64", []), + ((10, 5), "ppc64", ["ppc64", "fat64", "universal"]), + ((10, 3), "ppc64", []), + ((10, 17), "ppc", []), + ((10, 7), "ppc", []), + ((10, 6), "ppc", ["ppc", "fat32", "fat", "universal"]), + ((10, 0), "ppc", ["ppc", "fat32", "fat", "universal"]), + ((11, 0), "riscv", ["riscv", "universal"]), + ], ) - assert result[-1] == tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - - -def test_generic_abi(monkeypatch): - abi = sysconfig.get_config_var("SOABI") - if abi: - abi = abi.replace(".", "_").replace("-", "_") - else: - abi = "none" - assert abi == tags._generic_abi() - - monkeypatch.setattr(sysconfig, "get_config_var", lambda key: "cpython-37m-darwin") - assert tags._generic_abi() == "cpython_37m_darwin" - - monkeypatch.setattr(sysconfig, "get_config_var", lambda key: None) - assert tags._generic_abi() == "none" - - -def test_pypy_interpreter(monkeypatch): - if hasattr(sys, "pypy_version_info"): - major, minor = sys.pypy_version_info[:2] - else: - attributes = ["major", "minor", "micro", "releaselevel", "serial"] - PyPyVersion = collections.namedtuple("version_info", attributes) - major, minor = 6, 0 - pypy_version = PyPyVersion( - major=major, minor=minor, micro=1, releaselevel="final", serial=0 + def test_binary_formats(self, version, arch, expected): + assert tags._mac_binary_formats(version, arch) == expected + + def test_version_detection(self, monkeypatch): + if platform.system() != "Darwin": + monkeypatch.setattr( + platform, "mac_ver", lambda: ("10.14", ("", "", ""), "x86_64") + ) + version = platform.mac_ver()[0].split(".") + expected = "macosx_{major}_{minor}".format(major=version[0], minor=version[1]) + platforms = list(tags.mac_platforms(arch="x86_64")) + assert platforms[0].startswith(expected) + + @pytest.mark.parametrize("arch", ["x86_64", "i386"]) + def test_arch_detection(self, arch, monkeypatch): + if platform.system() != "Darwin" or platform.mac_ver()[2] != arch: + monkeypatch.setattr( + platform, "mac_ver", lambda: ("10.14", ("", "", ""), arch) + ) + monkeypatch.setattr(tags, "_mac_arch", lambda *args: arch) + assert next(tags.mac_platforms((10, 14))).endswith(arch) + + def test_mac_platforms(self): + platforms = list(tags.mac_platforms((10, 5), "x86_64")) + assert platforms == [ + "macosx_10_5_x86_64", + "macosx_10_5_intel", + "macosx_10_5_fat64", + "macosx_10_5_fat32", + "macosx_10_5_universal", + "macosx_10_4_x86_64", + "macosx_10_4_intel", + "macosx_10_4_fat64", + "macosx_10_4_fat32", + "macosx_10_4_universal", + ] + + assert len(list(tags.mac_platforms((10, 17), "x86_64"))) == 14 * 5 + + assert not list(tags.mac_platforms((10, 0), "x86_64")) + + +class TestManylinuxPlatform: + def test_module_declaration_true(self, manylinux_module): + manylinux_module.manylinux1_compatible = True + assert tags._is_manylinux_compatible("manylinux1", (2, 5)) + + def test_module_declaration_false(self, manylinux_module): + manylinux_module.manylinux1_compatible = False + assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) + + def test_module_declaration_missing_attribute(self, manylinux_module): + try: + del manylinux_module.manylinux1_compatible + except AttributeError: + pass + assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) + + def test_is_manylinux_compatible_module_support( + self, manylinux_module, monkeypatch + ): + monkeypatch.setitem(sys.modules, manylinux_module.__name__, None) + assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) + + @pytest.mark.parametrize( + "version,compatible", (((2, 0), True), ((2, 5), True), ((2, 10), False)) + ) + def test_is_manylinux_compatible_glibc_support( + self, version, compatible, monkeypatch + ): + monkeypatch.setitem(sys.modules, "_manylinux", None) + monkeypatch.setattr( + tags, + "_have_compatible_glibc", + lambda major, minor: (major, minor) <= (2, 5), ) - monkeypatch.setattr(sys, "pypy_version_info", pypy_version, raising=False) - expected = "pp{}{}{}".format(sys.version_info[0], major, minor) - assert expected == tags._pypy_interpreter() - - -def test_pypy_tags(mock_interpreter_name, monkeypatch): - if mock_interpreter_name("PyPy"): - monkeypatch.setattr(tags, "_pypy_interpreter", lambda: "pp360") - interpreter = tags._pypy_interpreter() - result = list(tags._pypy_tags((3, 3), interpreter, "pypy3_60", ["plat1", "plat2"])) - assert result == [ - tags.Tag(interpreter, "pypy3_60", "plat1"), - tags.Tag(interpreter, "pypy3_60", "plat2"), - tags.Tag(interpreter, "none", "plat1"), - tags.Tag(interpreter, "none", "plat2"), - ] - - -def test_sys_tags_on_mac_pypy(mock_interpreter_name, monkeypatch): - if mock_interpreter_name("PyPy"): - monkeypatch.setattr(tags, "_pypy_interpreter", lambda: "pp360") - if platform.system() != "Darwin": - monkeypatch.setattr(platform, "system", lambda: "Darwin") - monkeypatch.setattr(tags, "_mac_platforms", lambda: ["macosx_10_5_x86_64"]) - interpreter = tags._pypy_interpreter() - abi = tags._generic_abi() - platforms = tags._mac_platforms() - result = list(tags.sys_tags()) - assert result[0] == tags.Tag(interpreter, abi, platforms[0]) - assert result[-1] == tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - - -def test_generic_interpreter(): - version = sysconfig.get_config_var("py_version_nodot") - if not version: - version = "".join(sys.version_info[:2]) - result = tags._generic_interpreter("sillywalk", sys.version_info[:2]) - assert result == "sillywalk{version}".format(version=version) - - -def test_generic_interpreter_no_config_var(monkeypatch): - monkeypatch.setattr(sysconfig, "get_config_var", lambda _: None) - assert tags._generic_interpreter("sillywalk", (3, 6)) == "sillywalk36" - - -def test_generic_platforms(): - platform = distutils.util.get_platform().replace("-", "_") - platform = platform.replace(".", "_") - assert tags._generic_platforms() == [platform] - - -def test_generic_tags(): - result = list(tags._generic_tags("sillywalk33", (3, 3), "abi", ["plat1", "plat2"])) - assert result == [ - tags.Tag("sillywalk33", "abi", "plat1"), - tags.Tag("sillywalk33", "abi", "plat2"), - tags.Tag("sillywalk33", "none", "plat1"), - tags.Tag("sillywalk33", "none", "plat2"), - ] - - no_abi = tags._generic_tags("sillywalk34", (3, 4), "none", ["plat1", "plat2"]) - assert list(no_abi) == [ - tags.Tag("sillywalk34", "none", "plat1"), - tags.Tag("sillywalk34", "none", "plat2"), - ] - - -def test_sys_tags_on_windows_cpython(mock_interpreter_name, monkeypatch): - if mock_interpreter_name("CPython"): - monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) - if platform.system() != "Windows": - monkeypatch.setattr(platform, "system", lambda: "Windows") - monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"]) - abis = tags._cpython_abis(sys.version_info[:2]) - platforms = tags._generic_platforms() - result = list(tags.sys_tags()) - interpreter = "cp{major}{minor}".format( - major=sys.version_info[0], minor=sys.version_info[1] + assert bool(tags._is_manylinux_compatible("manylinux1", version)) == compatible + + @pytest.mark.parametrize( + "version_str,major,minor,expected", + [ + ("2.4", 2, 4, True), + ("2.4", 2, 5, False), + ("2.4", 2, 3, True), + ("3.4", 2, 4, False), + ], ) - assert len(abis) == 1 - expected = tags.Tag(interpreter, abis[0], platforms[0]) - assert result[0] == expected - expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - assert result[-1] == expected - - -def test_is_manylinux_compatible_module_support(monkeypatch): - monkeypatch.setattr(tags, "_have_compatible_glibc", lambda *args: False) - module_name = "_manylinux" - module = types.ModuleType(module_name) - module.manylinux1_compatible = True - monkeypatch.setitem(sys.modules, module_name, module) - assert tags._is_manylinux_compatible("manylinux1", (2, 5)) - module.manylinux1_compatible = False - assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) - del module.manylinux1_compatible - assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) - monkeypatch.setitem(sys.modules, module_name, None) - assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) - - -def test_is_manylinux_compatible_glibc_support(monkeypatch): - monkeypatch.setitem(sys.modules, "_manylinux", None) - monkeypatch.setattr( - tags, "_have_compatible_glibc", lambda major, minor: (major, minor) <= (2, 5) + def test_check_glibc_version(self, version_str, major, minor, expected): + assert expected == tags._check_glibc_version(version_str, major, minor) + + @pytest.mark.parametrize("version_str", ["glibc-2.4.5", "2"]) + def test_check_glibc_version_warning(self, version_str): + with warnings.catch_warnings(record=True) as w: + tags._check_glibc_version(version_str, 2, 4) + assert len(w) == 1 + assert issubclass(w[0].category, RuntimeWarning) + + @pytest.mark.skipif(not ctypes, reason="requires ctypes") + @pytest.mark.parametrize( + "version_str,expected", + [ + # Be very explicit about bytes and Unicode for Python 2 testing. + (b"2.4", "2.4"), + (u"2.4", "2.4"), + ], ) - assert tags._is_manylinux_compatible("manylinux1", (2, 0)) - assert tags._is_manylinux_compatible("manylinux1", (2, 5)) - assert not tags._is_manylinux_compatible("manylinux1", (2, 10)) - - -@pytest.mark.parametrize( - "version_str,major,minor,expected", - [ - ("2.4", 2, 4, True), - ("2.4", 2, 5, False), - ("2.4", 2, 3, True), - ("3.4", 2, 4, False), - ], -) -def test_check_glibc_version(version_str, major, minor, expected): - assert expected == tags._check_glibc_version(version_str, major, minor) - - -@pytest.mark.parametrize("version_str", ["glibc-2.4.5", "2"]) -def test_check_glibc_version_warning(version_str): - with warnings.catch_warnings(record=True) as w: - tags._check_glibc_version(version_str, 2, 4) - assert len(w) == 1 - assert issubclass(w[0].category, RuntimeWarning) - + def test_glibc_version_string(self, version_str, expected, monkeypatch): + class LibcVersion: + def __init__(self, version_str): + self.version_str = version_str -@pytest.mark.skipif(not ctypes, reason="requires ctypes") -@pytest.mark.parametrize( - "version_str,expected", - [ - # Be very explicit about bytes and Unicode for Python 2 testing. - (b"2.4", "2.4"), - (u"2.4", "2.4"), - ], -) -def test_glibc_version_string(version_str, expected, monkeypatch): - class LibcVersion: - def __init__(self, version_str): - self.version_str = version_str - - def __call__(self): - return version_str - - class ProcessNamespace: - def __init__(self, libc_version): - self.gnu_get_libc_version = libc_version - - process_namespace = ProcessNamespace(LibcVersion(version_str)) - monkeypatch.setattr(ctypes, "CDLL", lambda _: process_namespace) - monkeypatch.setattr(tags, "_glibc_version_string_confstr", lambda: False) - - assert tags._glibc_version_string() == expected - - del process_namespace.gnu_get_libc_version - assert tags._glibc_version_string() is None + def __call__(self): + return version_str + class ProcessNamespace: + def __init__(self, libc_version): + self.gnu_get_libc_version = libc_version -def test_glibc_version_string_confstr(monkeypatch): - monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False) - assert tags._glibc_version_string_confstr() == "2.20" + process_namespace = ProcessNamespace(LibcVersion(version_str)) + monkeypatch.setattr(ctypes, "CDLL", lambda _: process_namespace) + monkeypatch.setattr(tags, "_glibc_version_string_confstr", lambda: False) + assert tags._glibc_version_string() == expected -@pytest.mark.parametrize( - "failure", [pretend.raiser(ValueError), pretend.raiser(OSError), lambda x: "XXX"] -) -def test_glibc_version_string_confstr_fail(monkeypatch, failure): - monkeypatch.setattr(os, "confstr", failure, raising=False) - assert tags._glibc_version_string_confstr() is None - - -def test_glibc_version_string_confstr_missing(monkeypatch): - monkeypatch.delattr(os, "confstr", raising=False) - assert tags._glibc_version_string_confstr() is None - + del process_namespace.gnu_get_libc_version + assert tags._glibc_version_string() is None -def test_glibc_version_string_ctypes_missing(monkeypatch): - monkeypatch.setitem(sys.modules, "ctypes", None) - assert tags._glibc_version_string_ctypes() is None + def test_glibc_version_string_confstr(self, monkeypatch): + monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False) + assert tags._glibc_version_string_confstr() == "2.20" - -def test_get_config_var_does_not_log(monkeypatch): - debug = pretend.call_recorder(lambda *a: None) - monkeypatch.setattr(tags.logger, "debug", debug) - tags._get_config_var("missing") - assert debug.calls == [] - - -def test_get_config_var_does_log(monkeypatch): - debug = pretend.call_recorder(lambda *a: None) - monkeypatch.setattr(tags.logger, "debug", debug) - tags._get_config_var("missing", warn=True) - assert debug.calls == [ - pretend.call( - "Config variable '%s' is unset, Python ABI tag may be incorrect", "missing" - ) - ] - - -def test_have_compatible_glibc(monkeypatch): - if platform.system() == "Linux": + @pytest.mark.parametrize( + "failure", + [pretend.raiser(ValueError), pretend.raiser(OSError), lambda x: "XXX"], + ) + def test_glibc_version_string_confstr_fail(self, monkeypatch, failure): + monkeypatch.setattr(os, "confstr", failure, raising=False) + assert tags._glibc_version_string_confstr() is None + + def test_glibc_version_string_confstr_missing(self, monkeypatch): + monkeypatch.delattr(os, "confstr", raising=False) + assert tags._glibc_version_string_confstr() is None + + def test_glibc_version_string_ctypes_missing(self, monkeypatch): + monkeypatch.setitem(sys.modules, "ctypes", None) + assert tags._glibc_version_string_ctypes() is None + + def test_get_config_var_does_not_log(self, monkeypatch): + debug = pretend.call_recorder(lambda *a: None) + monkeypatch.setattr(tags.logger, "debug", debug) + tags._get_config_var("missing") + assert debug.calls == [] + + def test_get_config_var_does_log(self, monkeypatch): + debug = pretend.call_recorder(lambda *a: None) + monkeypatch.setattr(tags.logger, "debug", debug) + tags._get_config_var("missing", warn=True) + assert debug.calls == [ + pretend.call( + "Config variable '%s' is unset, Python ABI tag may be incorrect", + "missing", + ) + ] + + @pytest.mark.skipif(platform.system() != "Linux", reason="requires Linux") + def test_have_compatible_glibc_linux(self): # Assuming no one is running this test with a version of glibc released in # 1997. assert tags._have_compatible_glibc(2, 0) - else: + + def test_have_compatible_glibc(self, monkeypatch): monkeypatch.setattr(tags, "_glibc_version_string", lambda: "2.4") assert tags._have_compatible_glibc(2, 4) - monkeypatch.setattr(tags, "_glibc_version_string", lambda: None) - assert not tags._have_compatible_glibc(2, 4) - -def test_linux_platforms_64bit_on_64bit_os(is_64bit_os, is_x86, monkeypatch): - if platform.system() != "Linux" or not is_64bit_os or not is_x86: + def test_glibc_version_string_none(self, monkeypatch): + monkeypatch.setattr(tags, "_glibc_version_string", lambda: None) + assert not tags._have_compatible_glibc(2, 4) + + def test_linux_platforms_64bit_on_64bit_os(self, is_64bit_os, is_x86, monkeypatch): + if platform.system() != "Linux" or not is_64bit_os or not is_x86: + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) + linux_platform = list(tags._linux_platforms(is_32bit=False))[-1] + assert linux_platform == "linux_x86_64" + + def test_linux_platforms_32bit_on_64bit_os(self, is_64bit_os, is_x86, monkeypatch): + if platform.system() != "Linux" or not is_64bit_os or not is_x86: + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) + linux_platform = list(tags._linux_platforms(is_32bit=True))[-1] + assert linux_platform == "linux_i686" + + def test_linux_platforms_manylinux_unsupported(self, monkeypatch): monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) - linux_platform = tags._linux_platforms(is_32bit=False)[-1] - assert linux_platform == "linux_x86_64" + linux_platform = list(tags._linux_platforms(is_32bit=False)) + assert linux_platform == ["linux_x86_64"] + def test_linux_platforms_manylinux1(self, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux1" + ) + if platform.system() != "Linux": + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + platforms = list(tags._linux_platforms(is_32bit=False)) + assert platforms == ["manylinux1_x86_64", "linux_x86_64"] -def test_linux_platforms_32bit_on_64bit_os(is_64bit_os, is_x86, monkeypatch): - if platform.system() != "Linux" or not is_64bit_os or not is_x86: - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) - linux_platform = tags._linux_platforms(is_32bit=True)[-1] - assert linux_platform == "linux_i686" - + def test_linux_platforms_manylinux2010(self, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2010" + ) + if platform.system() != "Linux": + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + platforms = list(tags._linux_platforms(is_32bit=False)) + expected = ["manylinux2010_x86_64", "manylinux1_x86_64", "linux_x86_64"] + assert platforms == expected -def test_linux_platforms_manylinux_unsupported(monkeypatch): - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) - linux_platform = tags._linux_platforms(is_32bit=False) - assert linux_platform == ["linux_x86_64"] + def test_linux_platforms_manylinux2014(self, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + ) + if platform.system() != "Linux": + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + platforms = list(tags._linux_platforms(is_32bit=False)) + expected = [ + "manylinux2014_x86_64", + "manylinux2010_x86_64", + "manylinux1_x86_64", + "linux_x86_64", + ] + assert platforms == expected -def test_linux_platforms_manylinux1(monkeypatch): - monkeypatch.setattr( - tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux1" - ) - if platform.system() != "Linux": - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - platforms = tags._linux_platforms(is_32bit=False) - assert platforms == ["manylinux1_x86_64", "linux_x86_64"] +@pytest.mark.parametrize( + "platform_name,dispatch_func", + [ + ("Darwin", "mac_platforms"), + ("Linux", "_linux_platforms"), + ("Generic", "_generic_platforms"), + ], +) +def test__platform_tags(platform_name, dispatch_func, monkeypatch): + expected = ["sillywalk"] + monkeypatch.setattr(platform, "system", lambda: platform_name) + monkeypatch.setattr(tags, dispatch_func, lambda: expected) + assert tags._platform_tags() == expected -def test_linux_platforms_manylinux2010(monkeypatch): - monkeypatch.setattr( - tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2010" +class TestCPythonABI: + @pytest.mark.parametrize( + "py_debug,gettotalrefcount,result", + [(1, False, True), (0, False, False), (None, True, True)], ) - if platform.system() != "Linux": - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - platforms = tags._linux_platforms(is_32bit=False) - expected = ["manylinux2010_x86_64", "manylinux1_x86_64", "linux_x86_64"] - assert platforms == expected - - -def test_linux_platforms_manylinux2014(monkeypatch): - monkeypatch.setattr( - tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + def test_debug(self, py_debug, gettotalrefcount, result, monkeypatch): + config = {"Py_DEBUG": py_debug, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": 2} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + if gettotalrefcount: + monkeypatch.setattr(sys, "gettotalrefcount", 1, raising=False) + expected = ["cp37d" if result else "cp37"] + assert tags._cpython_abis((3, 7)) == expected + + def test_debug_file_extension(self, monkeypatch): + config = {"Py_DEBUG": None} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + monkeypatch.delattr(sys, "gettotalrefcount", raising=False) + monkeypatch.setattr(tags, "EXTENSION_SUFFIXES", {"_d.pyd"}) + assert tags._cpython_abis((3, 8)) == ["cp38d", "cp38"] + + @pytest.mark.parametrize( + "debug,expected", [(True, ["cp38d", "cp38"]), (False, ["cp38"])] ) - if platform.system() != "Linux": - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - platforms = tags._linux_platforms(is_32bit=False) - expected = [ - "manylinux2014_x86_64", - "manylinux2010_x86_64", - "manylinux1_x86_64", - "linux_x86_64", - ] - assert platforms == expected - - -def test_sys_tags_linux_cpython(mock_interpreter_name, monkeypatch): - if mock_interpreter_name("CPython"): - monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) - if platform.system() != "Linux": - monkeypatch.setattr(platform, "system", lambda: "Linux") - monkeypatch.setattr(tags, "_linux_platforms", lambda: ["linux_x86_64"]) - abis = tags._cpython_abis(sys.version_info[:2]) - platforms = tags._linux_platforms() - result = list(tags.sys_tags()) - expected_interpreter = "cp{major}{minor}".format( - major=sys.version_info[0], minor=sys.version_info[1] + def test__debug_cp38(self, debug, expected, monkeypatch): + config = {"Py_DEBUG": debug} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + assert tags._cpython_abis((3, 8)) == expected + + @pytest.mark.parametrize( + "pymalloc,version,result", + [ + (1, (3, 7), True), + (0, (3, 7), False), + (None, (3, 7), True), + (1, (3, 8), False), + ], + ) + def test_pymalloc(self, pymalloc, version, result, monkeypatch): + config = {"Py_DEBUG": 0, "WITH_PYMALLOC": pymalloc, "Py_UNICODE_SIZE": 2} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + base_abi = "cp{}{}".format(version[0], version[1]) + expected = [base_abi + "m" if result else base_abi] + assert tags._cpython_abis(version) == expected + + @pytest.mark.parametrize( + "unicode_size,maxunicode,version,result", + [ + (4, 0x10FFFF, (3, 2), True), + (2, 0xFFFF, (3, 2), False), + (None, 0x10FFFF, (3, 2), True), + (None, 0xFFFF, (3, 2), False), + (4, 0x10FFFF, (3, 3), False), + ], ) - assert len(abis) == 1 - assert result[0] == tags.Tag(expected_interpreter, abis[0], platforms[0]) - expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - assert result[-1] == expected + def test_wide_unicode(self, unicode_size, maxunicode, version, result, monkeypatch): + config = {"Py_DEBUG": 0, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": unicode_size} + monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) + monkeypatch.setattr(sys, "maxunicode", maxunicode) + base_abi = "cp{}{}".format(version[0], version[1]) + expected = [base_abi + "u" if result else base_abi] + assert tags._cpython_abis(version) == expected + + +class TestCPythonTags: + def test_iterator_returned(self): + result_iterator = tags.cpython_tags( + (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"] + ) + isinstance(result_iterator, collections_abc.Iterator) + def test_all_args(self): + result_iterator = tags.cpython_tags( + (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"] + ) + result = list(result_iterator) + assert result == [ + tags.Tag("cp38", "cp38d", "plat1"), + tags.Tag("cp38", "cp38d", "plat2"), + tags.Tag("cp38", "cp38", "plat1"), + tags.Tag("cp38", "cp38", "plat2"), + tags.Tag("cp38", "abi3", "plat1"), + tags.Tag("cp38", "abi3", "plat2"), + tags.Tag("cp38", "none", "plat1"), + tags.Tag("cp38", "none", "plat2"), + tags.Tag("cp37", "abi3", "plat1"), + tags.Tag("cp37", "abi3", "plat2"), + tags.Tag("cp36", "abi3", "plat1"), + tags.Tag("cp36", "abi3", "plat2"), + tags.Tag("cp35", "abi3", "plat1"), + tags.Tag("cp35", "abi3", "plat2"), + tags.Tag("cp34", "abi3", "plat1"), + tags.Tag("cp34", "abi3", "plat2"), + tags.Tag("cp33", "abi3", "plat1"), + tags.Tag("cp33", "abi3", "plat2"), + tags.Tag("cp32", "abi3", "plat1"), + tags.Tag("cp32", "abi3", "plat2"), + ] + result = list(tags.cpython_tags((3, 3), ["cp33m"], ["plat1", "plat2"])) + assert result == [ + tags.Tag("cp33", "cp33m", "plat1"), + tags.Tag("cp33", "cp33m", "plat2"), + tags.Tag("cp33", "abi3", "plat1"), + tags.Tag("cp33", "abi3", "plat2"), + tags.Tag("cp33", "none", "plat1"), + tags.Tag("cp33", "none", "plat2"), + tags.Tag("cp32", "abi3", "plat1"), + tags.Tag("cp32", "abi3", "plat2"), + ] + + def test_python_version_defaults(self): + tag = next(tags.cpython_tags(abis=["abi3"], platforms=["any"])) + interpreter = "cp{}{}".format(*sys.version_info[:2]) + assert tag == tags.Tag(interpreter, "abi3", "any") + + def test_abi_defaults(self, monkeypatch): + monkeypatch.setattr(tags, "_cpython_abis", lambda _1, _2: ["cp38"]) + result = list(tags.cpython_tags((3, 8), platforms=["any"])) + assert tags.Tag("cp38", "cp38", "any") in result + assert tags.Tag("cp38", "abi3", "any") in result + assert tags.Tag("cp38", "none", "any") in result + + def test_platforms_defaults(self, monkeypatch): + monkeypatch.setattr(tags, "_platform_tags", lambda: ["plat1"]) + result = list(tags.cpython_tags((3, 8), abis=["whatever"])) + assert tags.Tag("cp38", "whatever", "plat1") in result + + def test_major_only_python_version(self): + result = list(tags.cpython_tags((3,), ["abi"], ["plat"])) + assert result == [ + tags.Tag("cp3", "abi", "plat"), + tags.Tag("cp3", "none", "plat"), + ] + + def test_major_only_python_version_with_default_abis(self): + result = list(tags.cpython_tags((3,), platforms=["plat"])) + assert result == [tags.Tag("cp3", "none", "plat")] + + @pytest.mark.parametrize("abis", [[], ["abi3"], ["none"]]) + def test_skip_redundant_abis(self, abis): + results = list(tags.cpython_tags((3, 0), abis=abis, platforms=["any"])) + assert results == [ + tags.Tag("cp30", "abi3", "any"), + tags.Tag("cp30", "none", "any"), + ] + + +class TestGenericTags: + @pytest.mark.skipif( + not sysconfig.get_config_var("SOABI"), reason="SOABI not defined" + ) + def test__generic_abi_soabi_provided(self): + abi = sysconfig.get_config_var("SOABI").replace(".", "_").replace("-", "_") + assert [abi] == list(tags._generic_abi()) -def test_generic_sys_tags(monkeypatch): - monkeypatch.setattr(platform, "system", lambda: "Generic") - monkeypatch.setattr(tags, "_interpreter_name", lambda: "generic") + def test__generic_abi(self, monkeypatch): + monkeypatch.setattr( + sysconfig, "get_config_var", lambda key: "cpython-37m-darwin" + ) + assert list(tags._generic_abi()) == ["cpython_37m_darwin"] + + def test__generic_abi_no_soabi(self, monkeypatch): + monkeypatch.setattr(sysconfig, "get_config_var", lambda key: None) + assert not list(tags._generic_abi()) + + def test_generic_platforms(self): + platform = distutils.util.get_platform().replace("-", "_") + platform = platform.replace(".", "_") + assert list(tags._generic_platforms()) == [platform] + + def test_iterator_returned(self): + result_iterator = tags.generic_tags("sillywalk33", ["abi"], ["plat1", "plat2"]) + assert isinstance(result_iterator, collections_abc.Iterator) + + def test_all_args(self): + result_iterator = tags.generic_tags("sillywalk33", ["abi"], ["plat1", "plat2"]) + result = list(result_iterator) + assert result == [ + tags.Tag("sillywalk33", "abi", "plat1"), + tags.Tag("sillywalk33", "abi", "plat2"), + tags.Tag("sillywalk33", "none", "plat1"), + tags.Tag("sillywalk33", "none", "plat2"), + ] + + @pytest.mark.parametrize("abi", [[], ["none"]]) + def test_abi_unspecified(self, abi): + no_abi = list(tags.generic_tags("sillywalk34", abi, ["plat1", "plat2"])) + assert no_abi == [ + tags.Tag("sillywalk34", "none", "plat1"), + tags.Tag("sillywalk34", "none", "plat2"), + ] + + def test_interpreter_default(self, monkeypatch): + monkeypatch.setattr(tags, "interpreter_name", lambda: "sillywalk") + monkeypatch.setattr(tags, "interpreter_version", lambda warn: "NN") + result = list(tags.generic_tags(abis=["none"], platforms=["any"])) + assert result == [tags.Tag("sillywalkNN", "none", "any")] + + def test_abis_default(self, monkeypatch): + monkeypatch.setattr(tags, "_generic_abi", lambda: iter(["abi"])) + result = list(tags.generic_tags(interpreter="sillywalk", platforms=["any"])) + assert result == [ + tags.Tag("sillywalk", "abi", "any"), + tags.Tag("sillywalk", "none", "any"), + ] + + def test_platforms_default(self, monkeypatch): + monkeypatch.setattr(tags, "_platform_tags", lambda: ["plat"]) + result = list(tags.generic_tags(interpreter="sillywalk", abis=["none"])) + assert result == [tags.Tag("sillywalk", "none", "plat")] + + +class TestCompatibleTags: + def test_all_args(self): + result = list(tags.compatible_tags((3, 3), "cp33", ["plat1", "plat2"])) + assert result == [ + tags.Tag("py33", "none", "plat1"), + tags.Tag("py33", "none", "plat2"), + tags.Tag("py3", "none", "plat1"), + tags.Tag("py3", "none", "plat2"), + tags.Tag("py32", "none", "plat1"), + tags.Tag("py32", "none", "plat2"), + tags.Tag("py31", "none", "plat1"), + tags.Tag("py31", "none", "plat2"), + tags.Tag("py30", "none", "plat1"), + tags.Tag("py30", "none", "plat2"), + tags.Tag("cp33", "none", "any"), + tags.Tag("py33", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py32", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + def test_major_only_python_version(self): + result = list(tags.compatible_tags((3,), "cp33", ["plat"])) + assert result == [ + tags.Tag("py3", "none", "plat"), + tags.Tag("cp33", "none", "any"), + tags.Tag("py3", "none", "any"), + ] + + def test_default_python_version(self, monkeypatch): + monkeypatch.setattr(sys, "version_info", (3, 1)) + result = list(tags.compatible_tags(interpreter="cp31", platforms=["plat"])) + assert result == [ + tags.Tag("py31", "none", "plat"), + tags.Tag("py3", "none", "plat"), + tags.Tag("py30", "none", "plat"), + tags.Tag("cp31", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + def test_default_interpreter(self): + result = list(tags.compatible_tags((3, 1), platforms=["plat"])) + assert result == [ + tags.Tag("py31", "none", "plat"), + tags.Tag("py3", "none", "plat"), + tags.Tag("py30", "none", "plat"), + tags.Tag("py31", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + def test_default_platforms(self, monkeypatch): + monkeypatch.setattr(tags, "_platform_tags", lambda: ["plat"]) + result = list(tags.compatible_tags((3, 1), "cp31")) + assert result == [ + tags.Tag("py31", "none", "plat"), + tags.Tag("py3", "none", "plat"), + tags.Tag("py30", "none", "plat"), + tags.Tag("cp31", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + +class TestSysTags: + @pytest.mark.parametrize( + "name,expected", + [("CPython", "cp"), ("PyPy", "pp"), ("Jython", "jy"), ("IronPython", "ip")], + ) + def test_interpreter_name(self, name, expected, mock_interpreter_name): + mock_interpreter_name(name) + assert tags.interpreter_name() == expected + + def test_iterator(self): + assert isinstance(tags.sys_tags(), collections_abc.Iterator) + + def test_mac_cpython(self, mock_interpreter_name, monkeypatch): + if mock_interpreter_name("CPython"): + monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) + if platform.system() != "Darwin": + monkeypatch.setattr(platform, "system", lambda: "Darwin") + monkeypatch.setattr(tags, "mac_platforms", lambda: ["macosx_10_5_x86_64"]) + abis = tags._cpython_abis(sys.version_info[:2]) + platforms = list(tags.mac_platforms()) + result = list(tags.sys_tags()) + assert len(abis) == 1 + assert result[0] == tags.Tag( + "cp{major}{minor}".format( + major=sys.version_info[0], minor=sys.version_info[1] + ), + abis[0], + platforms[0], + ) + assert result[-1] == tags.Tag( + "py{}0".format(sys.version_info[0]), "none", "any" + ) - result = list(tags.sys_tags()) - expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") - assert result[-1] == expected + def test_windows_cpython(self, mock_interpreter_name, monkeypatch): + if mock_interpreter_name("CPython"): + monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) + if platform.system() != "Windows": + monkeypatch.setattr(platform, "system", lambda: "Windows") + monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"]) + abis = tags._cpython_abis(sys.version_info[:2]) + platforms = tags._generic_platforms() + result = list(tags.sys_tags()) + interpreter = "cp{major}{minor}".format( + major=sys.version_info[0], minor=sys.version_info[1] + ) + assert len(abis) == 1 + expected = tags.Tag(interpreter, abis[0], platforms[0]) + assert result[0] == expected + expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + assert result[-1] == expected + + def test_linux_cpython(self, mock_interpreter_name, monkeypatch): + if mock_interpreter_name("CPython"): + monkeypatch.setattr(tags, "_cpython_abis", lambda *a: ["cp33m"]) + if platform.system() != "Linux": + monkeypatch.setattr(platform, "system", lambda: "Linux") + monkeypatch.setattr(tags, "_linux_platforms", lambda: ["linux_x86_64"]) + abis = list(tags._cpython_abis(sys.version_info[:2])) + platforms = list(tags._linux_platforms()) + result = list(tags.sys_tags()) + expected_interpreter = "cp{major}{minor}".format( + major=sys.version_info[0], minor=sys.version_info[1] + ) + assert len(abis) == 1 + assert result[0] == tags.Tag(expected_interpreter, abis[0], platforms[0]) + expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + assert result[-1] == expected + + def test_generic(self, monkeypatch): + monkeypatch.setattr(platform, "system", lambda: "Generic") + monkeypatch.setattr(tags, "interpreter_name", lambda: "generic") + + result = list(tags.sys_tags()) + expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + assert result[-1] == expected From a916dcd495e5f5d3e541f555dbc8e54d8476ee2c Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Mon, 25 Nov 2019 11:14:53 -0800 Subject: [PATCH 073/114] Update changelog --- CHANGELOG.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7ff2f26fd..2f390191e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,14 @@ Changelog * Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (:issue:`193`) +* Expand upon the API provded by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (:issue:`187`) + +* Officially support Python 3.8 (:issue:`232`) + +* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (:issue:`226`) + +* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (:issue:`226`) + 19.2 - 2019-09-18 ~~~~~~~~~~~~~~~~~ From 6a09d4015b54f80762ff3ef1597a8b6740563c19 Mon Sep 17 00:00:00 2001 From: sangarshanan Date: Tue, 3 Dec 2019 00:02:15 +0530 Subject: [PATCH 074/114] Adding PyPy to the testing GH action (#243) --- .github/workflows/test.yml | 34 ++++++++++++++++++++++++++++++++-- packaging/utils.py | 2 +- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4adcd1af6..b9de188c7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,8 +9,8 @@ on: - '**.py' jobs: - test: - name: Test Python ${{ matrix.python_version }} on ${{ matrix.os }} + test-cpython: + name: Test CPython ${{ matrix.python_version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: @@ -36,3 +36,33 @@ jobs: run: | python -m coverage run --source packaging/ -m pytest --strict tests python -m coverage report -m --fail-under 100 + + + test-pypy: + name: Test ${{ matrix.pypy_version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + pypy_version: ['pypy2', 'pypy3'] + + steps: + - uses: actions/checkout@v1 + + - uses: actions/setup-python@v1 + name: Install ${{ matrix.pypy_version }} + with: + python-version: ${{ matrix.pypy_version }} + + - name: Install dependencies + # pytest >= 5.0.0 fails with pypy3 https://github.com/pytest-dev/pytest/issues/5807 + run: | + python -m pip install --upgrade pip + python -m pip install coverage pretend pytest==4.6.5 + + - name: Test coverage + run: | + python -m pytest --capture=no --strict + python -m coverage run --source packaging/ -m pytest --strict tests + python -m coverage report -m --fail-under 100 diff --git a/packaging/utils.py b/packaging/utils.py index 545772f72..44f1bf987 100644 --- a/packaging/utils.py +++ b/packaging/utils.py @@ -23,7 +23,7 @@ def canonicalize_name(name): 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. """ From 97764cd7ce8dee0414499532fb451b22fc953b63 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Wed, 11 Dec 2019 14:44:47 +0530 Subject: [PATCH 075/114] Use a secure variable for IRC channel --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f82bee88e..2119d0985 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,6 +37,10 @@ script: notifications: irc: channels: - - "irc.freenode.org#pypa-dev" + # This is set to a secure variable to prevent forks from notifying the + # IRC channel whenever they fail a build. This can be removed when travis + # implements https://github.com/travis-ci/travis-ci/issues/1094. + # The actual value here is: irc.freenode.org#pypa-dev + - secure: "Br6aYBYkjL17fBZ6+AczCkaBMWQAY6To8IH4zqhHrORXAWq4zeuC4VyCZ4MXmDzk0WbA3h3Ea7u9kodUf1sVK1h0q7HX66p8qmFyTncQoLFgo2LF/x1aU1FGWKDDSX5K6qKOzUKrHUhQyVq+uAuRVUm7bJhJL0/viPwEoh+bONo=" use_notice: true skip_join: true From 41d2d0de60e11d4aca4e0bff57f84185158bded5 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Tue, 17 Dec 2019 21:21:38 +0100 Subject: [PATCH 076/114] Restrict coverage version to '<5.0.0' (#247) --- .github/workflows/test.yml | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9de188c7..0e34c3e50 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install coverage pretend pytest + python -m pip install coverage<5.0.0 pretend pytest - name: Test coverage run: | @@ -59,7 +59,7 @@ jobs: # pytest >= 5.0.0 fails with pypy3 https://github.com/pytest-dev/pytest/issues/5807 run: | python -m pip install --upgrade pip - python -m pip install coverage pretend pytest==4.6.5 + python -m pip install coverage<5.0.0 pretend pytest==4.6.5 - name: Test coverage run: | diff --git a/tox.ini b/tox.ini index 68c011f00..c3c34f18c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py27,pypy,pypy3,py34,py35,py36,py37,py38,docs,lint [testenv] deps = - coverage + coverage<5.0.0 pretend pytest pip>=9.0.2 From 2a87d1c85f9666788f791b891762a454ac26272a Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Wed, 18 Dec 2019 00:57:22 +0100 Subject: [PATCH 077/114] Fix GitHub Actions workflow following #247 (#249) --- .github/workflows/test.yml | 13 ++++++++++--- tests/test_tags.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e34c3e50..7972dcddd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,9 +3,11 @@ name: Test on: pull_request: paths: + - '.github/workflows/test.yml' - '**.py' push: paths: + - '.github/workflows/test.yml' - '**.py' jobs: @@ -14,6 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macOS-latest] # Python 3.4 is not available from actions/setup-python@v1. @@ -30,12 +33,14 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install coverage<5.0.0 pretend pytest + python -m pip install 'coverage<5.0.0' pretend pytest + shell: bash - name: Test coverage run: | python -m coverage run --source packaging/ -m pytest --strict tests python -m coverage report -m --fail-under 100 + shell: bash test-pypy: @@ -43,6 +48,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macOS-latest] pypy_version: ['pypy2', 'pypy3'] @@ -56,13 +62,14 @@ jobs: python-version: ${{ matrix.pypy_version }} - name: Install dependencies - # pytest >= 5.0.0 fails with pypy3 https://github.com/pytest-dev/pytest/issues/5807 run: | python -m pip install --upgrade pip - python -m pip install coverage<5.0.0 pretend pytest==4.6.5 + python -m pip install 'coverage<5.0.0' pretend pytest + shell: bash - name: Test coverage run: | python -m pytest --capture=no --strict python -m coverage run --source packaging/ -m pytest --strict tests python -m coverage report -m --fail-under 100 + shell: bash diff --git a/tests/test_tags.py b/tests/test_tags.py index 0a89991e1..2ce3dc6ae 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -804,7 +804,7 @@ def test_windows_cpython(self, mock_interpreter_name, monkeypatch): monkeypatch.setattr(platform, "system", lambda: "Windows") monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"]) abis = tags._cpython_abis(sys.version_info[:2]) - platforms = tags._generic_platforms() + platforms = list(tags._generic_platforms()) result = list(tags.sys_tags()) interpreter = "cp{major}{minor}".format( major=sys.version_info[0], minor=sys.version_info[1] From 211bf32d7cc0763caa5118bd34b61efb625b95e8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 18 Dec 2019 20:43:01 +0200 Subject: [PATCH 078/114] Travis CI: Test using Python 3.8 final (#250) --- .travis.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2119d0985..bfa253d41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python cache: pip -python: 3.6 +python: 3.8 matrix: include: @@ -18,16 +18,11 @@ matrix: env: TOXENV=py36 - python: 3.7 env: TOXENV=py37 - dist: xenial - - python: 3.8-dev - env: TOXENV=py - dist: xenial + - python: 3.8 + env: TOXENV=py38 - env: TOXENV=lint - env: TOXENV=docs - allow_failures: - - python: 3.8-dev - install: - pip install tox From 27c7d9b0523f11735ba5b51b134237fb99c7cb3b Mon Sep 17 00:00:00 2001 From: Brett Cannon <54418+brettcannon@users.noreply.github.com> Date: Wed, 18 Dec 2019 16:25:39 -0800 Subject: [PATCH 079/114] Don't list abi3 for Python versions older than 3.2 (#248) --- .pre-commit-config.yaml | 2 +- packaging/tags.py | 25 +++++++++++++++++-------- tests/test_tags.py | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 936a21233..b7b208c9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.740 + rev: v0.750 hooks: - id: mypy exclude: '^(docs|tasks|tests)|setup\.py' diff --git a/packaging/tags.py b/packaging/tags.py index a719d4e73..db29deb58 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -146,6 +146,16 @@ def _normalize_string(string): return string.replace(".", "_").replace("-", "_") +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, warn=False): # type: (PythonVersion, bool) -> List[str] py_version = tuple(py_version) # To allow for version comparison. @@ -231,16 +241,13 @@ def cpython_tags( for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) - # Not worrying about the case of Python 3.2 or older being specified and - # thus having redundant tags thanks to the abi3 in-fill later on as - # 'packaging' doesn't directly support Python that far back. - if len(python_version) > 1: + 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. - if len(python_version) > 1: + + if _abi3_applies(python_version): for minor_version in range(python_version[1] - 1, 1, -1): for platform_ in platforms: interpreter = "cp{major}{minor}".format( @@ -431,9 +438,11 @@ def _glibc_version_string_confstr(): # 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("CS_GNU_LIBC_VERSION") + version_string = os.confstr( # type: ignore[attr-defined] # noqa: F821 + "CS_GNU_LIBC_VERSION" + ) assert version_string is not None - _, version = version_string.split() + _, 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 diff --git a/tests/test_tags.py b/tests/test_tags.py index 2ce3dc6ae..e17b4d075 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -597,7 +597,7 @@ def test_all_args(self): def test_python_version_defaults(self): tag = next(tags.cpython_tags(abis=["abi3"], platforms=["any"])) interpreter = "cp{}{}".format(*sys.version_info[:2]) - assert tag == tags.Tag(interpreter, "abi3", "any") + assert interpreter == tag.interpreter def test_abi_defaults(self, monkeypatch): monkeypatch.setattr(tags, "_cpython_abis", lambda _1, _2: ["cp38"]) @@ -625,9 +625,37 @@ def test_major_only_python_version_with_default_abis(self): @pytest.mark.parametrize("abis", [[], ["abi3"], ["none"]]) def test_skip_redundant_abis(self, abis): results = list(tags.cpython_tags((3, 0), abis=abis, platforms=["any"])) + assert results == [tags.Tag("cp30", "none", "any")] + + def test_abi3_python33(self): + results = list(tags.cpython_tags((3, 3), abis=["cp33"], platforms=["plat"])) + assert results == [ + tags.Tag("cp33", "cp33", "plat"), + tags.Tag("cp33", "abi3", "plat"), + tags.Tag("cp33", "none", "plat"), + tags.Tag("cp32", "abi3", "plat"), + ] + + def test_no_excess_abi3_python32(self): + results = list(tags.cpython_tags((3, 2), abis=["cp32"], platforms=["plat"])) + assert results == [ + tags.Tag("cp32", "cp32", "plat"), + tags.Tag("cp32", "abi3", "plat"), + tags.Tag("cp32", "none", "plat"), + ] + + def test_no_abi3_python31(self): + results = list(tags.cpython_tags((3, 1), abis=["cp31"], platforms=["plat"])) + assert results == [ + tags.Tag("cp31", "cp31", "plat"), + tags.Tag("cp31", "none", "plat"), + ] + + def test_no_abi3_python27(self): + results = list(tags.cpython_tags((2, 7), abis=["cp27"], platforms=["plat"])) assert results == [ - tags.Tag("cp30", "abi3", "any"), - tags.Tag("cp30", "none", "any"), + tags.Tag("cp27", "cp27", "plat"), + tags.Tag("cp27", "none", "plat"), ] @@ -803,7 +831,7 @@ def test_windows_cpython(self, mock_interpreter_name, monkeypatch): if platform.system() != "Windows": monkeypatch.setattr(platform, "system", lambda: "Windows") monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"]) - abis = tags._cpython_abis(sys.version_info[:2]) + abis = list(tags._cpython_abis(sys.version_info[:2])) platforms = list(tags._generic_platforms()) result = list(tags.sys_tags()) interpreter = "cp{major}{minor}".format( From 6a320ab0cb2fb8a6d13a4b432856e6ec17730d7f Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Thu, 2 Jan 2020 22:31:55 +0100 Subject: [PATCH 080/114] Update manylinux detection to be robust to incompatible ABIs (#221) `armv7l` machine overlaps multiple ABI (`armhf`, `armel`). The same goes for `i686` when running on `x86_64` kernel (`i686`, `x32`). This commit checks that ABI is compatible with the ones defined in PEP 513/571/599 --- MANIFEST.in | 3 + packaging/tags.py | 139 ++++++++++++++++++++++++++++-- tests/build-hello-world.sh | 39 +++++++++ tests/hello-world-armv7l-armel | Bin 0 -> 52 bytes tests/hello-world-armv7l-armhf | Bin 0 -> 52 bytes tests/hello-world-invalid-class | Bin 0 -> 52 bytes tests/hello-world-invalid-data | Bin 0 -> 52 bytes tests/hello-world-invalid-magic | Bin 0 -> 52 bytes tests/hello-world-s390x-s390x | Bin 0 -> 64 bytes tests/hello-world-too-short | Bin 0 -> 40 bytes tests/hello-world-x86_64-amd64 | Bin 0 -> 64 bytes tests/hello-world-x86_64-i386 | Bin 0 -> 52 bytes tests/hello-world-x86_64-x32 | Bin 0 -> 52 bytes tests/hello-world.c | 7 ++ tests/test_tags.py | 144 ++++++++++++++++++++++++++++++++ 15 files changed, 327 insertions(+), 5 deletions(-) create mode 100755 tests/build-hello-world.sh create mode 100755 tests/hello-world-armv7l-armel create mode 100755 tests/hello-world-armv7l-armhf create mode 100755 tests/hello-world-invalid-class create mode 100755 tests/hello-world-invalid-data create mode 100755 tests/hello-world-invalid-magic create mode 100644 tests/hello-world-s390x-s390x create mode 100644 tests/hello-world-too-short create mode 100644 tests/hello-world-x86_64-amd64 create mode 100755 tests/hello-world-x86_64-i386 create mode 100755 tests/hello-world-x86_64-x32 create mode 100644 tests/hello-world.c diff --git a/MANIFEST.in b/MANIFEST.in index b25e07800..19afb627c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,9 +8,12 @@ include tox.ini recursive-include docs * recursive-include tests *.py +recursive-include tests hello-world-* exclude .travis.yml exclude dev-requirements.txt +exclude tests/build-hello-world.sh +exclude tests/hello-world.c prune docs/_build prune tasks diff --git a/packaging/tags.py b/packaging/tags.py index db29deb58..20512aaf9 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -17,6 +17,7 @@ import os import platform import re +import struct import sys import sysconfig import warnings @@ -27,6 +28,7 @@ from typing import ( Dict, FrozenSet, + IO, Iterable, Iterator, List, @@ -514,16 +516,143 @@ def _have_compatible_glibc(required_major, minimum_minor): 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" + format_i = "I" + format_q = "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) - ) + 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): diff --git a/tests/build-hello-world.sh b/tests/build-hello-world.sh new file mode 100755 index 000000000..9c3e1e181 --- /dev/null +++ b/tests/build-hello-world.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -x +set -e + +if [ $# -eq 0 ]; then + docker run --rm -v $(pwd):/home/hello-world arm32v5/debian /home/hello-world/build-hello-world.sh incontainer 52 + docker run --rm -v $(pwd):/home/hello-world arm32v7/debian /home/hello-world/build-hello-world.sh incontainer 52 + docker run --rm -v $(pwd):/home/hello-world i386/debian /home/hello-world/build-hello-world.sh incontainer 52 + docker run --rm -v $(pwd):/home/hello-world s390x/debian /home/hello-world/build-hello-world.sh incontainer 64 + docker run --rm -v $(pwd):/home/hello-world debian /home/hello-world/build-hello-world.sh incontainer 64 + docker run --rm -v $(pwd):/home/hello-world debian /home/hello-world/build-hello-world.sh x32 52 + cp -f hello-world-x86_64-i386 hello-world-invalid-magic + printf "\x00" | dd of=hello-world-invalid-magic bs=1 seek=0x00 count=1 conv=notrunc + cp -f hello-world-x86_64-i386 hello-world-invalid-class + printf "\x00" | dd of=hello-world-invalid-class bs=1 seek=0x04 count=1 conv=notrunc + cp -f hello-world-x86_64-i386 hello-world-invalid-data + printf "\x00" | dd of=hello-world-invalid-data bs=1 seek=0x05 count=1 conv=notrunc + head -c 40 hello-world-x86_64-i386 > hello-world-too-short + exit 0 +fi + +export DEBIAN_FRONTEND=noninteractive +cd /home/hello-world/ +apt-get update +apt-get install -y --no-install-recommends gcc libc6-dev +if [ "$1" == "incontainer" ]; then + ARCH=$(dpkg --print-architecture) + CFLAGS="" +else + ARCH=$1 + dpkg --add-architecture ${ARCH} + apt-get install -y --no-install-recommends gcc-multilib libc6-dev-${ARCH} + CFLAGS="-mx32" +fi +NAME=hello-world-$(uname -m)-${ARCH} +gcc -Os -s ${CFLAGS} -o ${NAME}-full hello-world.c +head -c $2 ${NAME}-full > ${NAME} +rm -f ${NAME}-full diff --git a/tests/hello-world-armv7l-armel b/tests/hello-world-armv7l-armel new file mode 100755 index 0000000000000000000000000000000000000000..1dfd23fa3c0eb944358b1eee0b7c241c79da3cd3 GIT binary patch literal 52 ucmb<-^>JflWMqH=W(Exg5Kn-Gfx!ewHwXd=CI(g$1_cIApqw;=6axTaoC6>L literal 0 HcmV?d00001 diff --git a/tests/hello-world-armv7l-armhf b/tests/hello-world-armv7l-armhf new file mode 100755 index 0000000000000000000000000000000000000000..965ab3003af97dc37d31878feeff574b59dae17b GIT binary patch literal 52 ucmb<-^>JflWMqH=W(Exg5KoYWfx!ewcL)Lr76w)m1_cIApqw;=6axTbb^|K_ literal 0 HcmV?d00001 diff --git a/tests/hello-world-invalid-class b/tests/hello-world-invalid-class new file mode 100755 index 0000000000000000000000000000000000000000..5e9899fc07609f37f5251f0da5a5af97d4d79234 GIT binary patch literal 52 tcmb<-^>JfhWMqH=W(H;k5O0A11A_^WHZX+9m@p_Xa5HEy$S_DV003%T17iRH literal 0 HcmV?d00001 diff --git a/tests/hello-world-invalid-data b/tests/hello-world-invalid-data new file mode 100755 index 0000000000000000000000000000000000000000..2659b8ee25768ba38932177ae91d4f44a482917a GIT binary patch literal 52 scmb<-^>JflU}S&+W(H<3Z-D>`!ywH70BT+XV*mgE literal 0 HcmV?d00001 diff --git a/tests/hello-world-invalid-magic b/tests/hello-world-invalid-magic new file mode 100755 index 0000000000000000000000000000000000000000..46066ad2ded1af7c68a4f5d4d3ccc66933b89f43 GIT binary patch literal 52 tcmZQ@^>JflWMqH=W(H;k5O0A11A_^WHZX+9m@p_Xa5HEy$S_DV002JfjVq|~=W(F}J8!EsS02OzD(1HbE+JV7>fs?_3L5e{V0D5TyfdBvi literal 0 HcmV?d00001 diff --git a/tests/hello-world-too-short b/tests/hello-world-too-short new file mode 100644 index 0000000000000000000000000000000000000000..4e5c0396b94d8f3c6b5ef6fcd1cc67317edd3e85 GIT binary patch literal 40 hcmb<-^>JflWMqH=W(H;k5O0A11A_^WHZX+9002cN0;T`} literal 0 HcmV?d00001 diff --git a/tests/hello-world-x86_64-amd64 b/tests/hello-world-x86_64-amd64 new file mode 100644 index 0000000000000000000000000000000000000000..c7f5b0b5e5bb69550d594b8d181f6f881356e404 GIT binary patch literal 64 tcmb<-^>JfjWMqH=W(GS35U)T0BH{p*@GyijfRqD+1p_yblwpu&0057p1JD2f literal 0 HcmV?d00001 diff --git a/tests/hello-world-x86_64-i386 b/tests/hello-world-x86_64-i386 new file mode 100755 index 0000000000000000000000000000000000000000..ff1d540a30c6df244d013bf411ad8ea63eb3f16f GIT binary patch literal 52 tcmb<-^>JflWMqH=W(H;k5O0A11A_^WHZX+9m@p_Xa5HEy$S_DV003%@17rXI literal 0 HcmV?d00001 diff --git a/tests/hello-world-x86_64-x32 b/tests/hello-world-x86_64-x32 new file mode 100755 index 0000000000000000000000000000000000000000..daf85d34737e237b7d1473929363340fb7402e3c GIT binary patch literal 52 scmb<-^>JflWMqH=W(GS35U)Uhfx!ew+ZaM*Oc)dxxEVAUWEi9w0B#cldjJ3c literal 0 HcmV?d00001 diff --git a/tests/hello-world.c b/tests/hello-world.c new file mode 100644 index 000000000..5e591c3ec --- /dev/null +++ b/tests/hello-world.c @@ -0,0 +1,7 @@ +#include + +int main(int argc, char* argv[]) +{ + printf("Hello world"); + return 0; +} diff --git a/tests/test_tags.py b/tests/test_tags.py index e17b4d075..1eacf6864 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -469,6 +469,150 @@ def test_linux_platforms_manylinux2014(self, monkeypatch): ] assert platforms == expected + def test_linux_platforms_manylinux2014_armhf_abi(self, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + ) + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_armv7l") + monkeypatch.setattr( + sys, + "executable", + os.path.join(os.path.dirname(__file__), "hello-world-armv7l-armhf"), + ) + platforms = list(tags._linux_platforms(is_32bit=True)) + expected = ["manylinux2014_armv7l", "linux_armv7l"] + assert platforms == expected + + def test_linux_platforms_manylinux2014_i386_abi(self, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + ) + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr( + sys, + "executable", + os.path.join(os.path.dirname(__file__), "hello-world-x86_64-i386"), + ) + platforms = list(tags._linux_platforms(is_32bit=True)) + expected = [ + "manylinux2014_i686", + "manylinux2010_i686", + "manylinux1_i686", + "linux_i686", + ] + assert platforms == expected + + def test_linux_platforms_manylinux2014_armv6l(self, monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" + ) + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_armv6l") + platforms = list(tags._linux_platforms(is_32bit=True)) + expected = ["linux_armv6l"] + assert platforms == expected + + @pytest.mark.parametrize( + "machine, abi, alt_machine", + [("x86_64", "x32", "i686"), ("armv7l", "armel", "armv7l")], + ) + def test_linux_platforms_not_manylinux_abi( + self, monkeypatch, machine, abi, alt_machine + ): + monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda name, _: True) + monkeypatch.setattr( + distutils.util, "get_platform", lambda: "linux_{}".format(machine) + ) + monkeypatch.setattr( + sys, + "executable", + os.path.join( + os.path.dirname(__file__), "hello-world-{}-{}".format(machine, abi) + ), + ) + platforms = list(tags._linux_platforms(is_32bit=True)) + expected = ["linux_{}".format(alt_machine)] + assert platforms == expected + + @pytest.mark.parametrize( + "machine, abi, elf_class, elf_data, elf_machine", + [ + ( + "x86_64", + "x32", + tags._ELFFileHeader.ELFCLASS32, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_X86_64, + ), + ( + "x86_64", + "i386", + tags._ELFFileHeader.ELFCLASS32, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_386, + ), + ( + "x86_64", + "amd64", + tags._ELFFileHeader.ELFCLASS64, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_X86_64, + ), + ( + "armv7l", + "armel", + tags._ELFFileHeader.ELFCLASS32, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_ARM, + ), + ( + "armv7l", + "armhf", + tags._ELFFileHeader.ELFCLASS32, + tags._ELFFileHeader.ELFDATA2LSB, + tags._ELFFileHeader.EM_ARM, + ), + ( + "s390x", + "s390x", + tags._ELFFileHeader.ELFCLASS64, + tags._ELFFileHeader.ELFDATA2MSB, + tags._ELFFileHeader.EM_S390, + ), + ], + ) + def test_get_elf_header( + self, monkeypatch, machine, abi, elf_class, elf_data, elf_machine + ): + path = os.path.join( + os.path.dirname(__file__), "hello-world-{}-{}".format(machine, abi) + ) + monkeypatch.setattr(sys, "executable", path) + elf_header = tags._get_elf_header() + assert elf_header.e_ident_class == elf_class + assert elf_header.e_ident_data == elf_data + assert elf_header.e_machine == elf_machine + + @pytest.mark.parametrize( + "content", [None, "invalid-magic", "invalid-class", "invalid-data", "too-short"] + ) + def test_get_elf_header_bad_excutable(self, monkeypatch, content): + if content: + path = os.path.join( + os.path.dirname(__file__), "hello-world-{}".format(content) + ) + else: + path = None + monkeypatch.setattr(sys, "executable", path) + assert tags._get_elf_header() is None + + def test_is_linux_armhf_not_elf(self, monkeypatch): + monkeypatch.setattr(tags, "_get_elf_header", lambda: None) + assert not tags._is_linux_armhf() + + def test_is_linux_i686_not_elf(self, monkeypatch): + monkeypatch.setattr(tags, "_get_elf_header", lambda: None) + assert not tags._is_linux_i686() + @pytest.mark.parametrize( "platform_name,dispatch_func", From 9963add2cb4053dd5ecc4925d4d4df6b4c4f5fe2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 6 Jan 2020 11:50:13 +0530 Subject: [PATCH 081/114] Prepare CHANGELOG --- CHANGELOG.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f390191e..81b7c6e6b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,11 +1,9 @@ Changelog --------- -20.0 - master_ +20.0 - 2020-01-06 ~~~~~~~~~~~~~~~~~ -.. note:: This version is not yet released and is under active development. - * Add type hints (:issue:`191`) * Add proper trove classifiers for PyPy support (:issue:`198`) From 47d40f640fddb7c97b01315419b6a1421d2dedbb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 6 Jan 2020 11:50:58 +0530 Subject: [PATCH 082/114] Bump version for release --- packaging/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/__about__.py b/packaging/__about__.py index 565565f81..269472837 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.0.dev0" +__version__ = "20.0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From e664cdd5c82b706ba4c5368477c8d7081fd99456 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 6 Jan 2020 11:54:05 +0530 Subject: [PATCH 083/114] Bump for development --- CHANGELOG.rst | 5 +++++ packaging/__about__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 81b7c6e6b..3241b8fe5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog --------- +20.1 - *unreleased* +~~~~~~~~~~~~~~~~~~~ + +No changes yet. + 20.0 - 2020-01-06 ~~~~~~~~~~~~~~~~~ diff --git a/packaging/__about__.py b/packaging/__about__.py index 269472837..e72680ad4 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.0" +__version__ = "20.1.dev0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From d49fdc500a7c057b71851847ff8d7cc92865bcf2 Mon Sep 17 00:00:00 2001 From: jeroendecroos Date: Tue, 21 Jan 2020 23:47:31 +0100 Subject: [PATCH 084/114] Don't have packaging.tags.compatible_tags() reuse an iterable (#258) The code was using an iterable in an inner loop which would lead to iterator exhaustion after the first time round the loop. Closes #257 --- packaging/tags.py | 5 ++--- tests/test_tags.py | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packaging/tags.py b/packaging/tags.py index 20512aaf9..60a69d8f9 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -315,7 +315,7 @@ def _py_interpreter_range(py_version): def compatible_tags( python_version=None, # type: Optional[PythonVersion] interpreter=None, # type: Optional[str] - platforms=None, # type: Optional[Iterator[str]] + platforms=None, # type: Optional[Iterable[str]] ): # type: (...) -> Iterator[Tag] """ @@ -328,8 +328,7 @@ def compatible_tags( """ if not python_version: python_version = sys.version_info[:2] - if not platforms: - platforms = _platform_tags() + platforms = list(platforms or _platform_tags()) for version in _py_interpreter_range(python_version): for platform_ in platforms: yield Tag(version, "none", platform_) diff --git a/tests/test_tags.py b/tests/test_tags.py index 1eacf6864..1c05969f7 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -923,12 +923,15 @@ def test_default_interpreter(self): ] def test_default_platforms(self, monkeypatch): - monkeypatch.setattr(tags, "_platform_tags", lambda: ["plat"]) + monkeypatch.setattr(tags, "_platform_tags", lambda: iter(["plat", "plat2"])) result = list(tags.compatible_tags((3, 1), "cp31")) assert result == [ tags.Tag("py31", "none", "plat"), + tags.Tag("py31", "none", "plat2"), tags.Tag("py3", "none", "plat"), + tags.Tag("py3", "none", "plat2"), tags.Tag("py30", "none", "plat"), + tags.Tag("py30", "none", "plat2"), tags.Tag("cp31", "none", "any"), tags.Tag("py31", "none", "any"), tags.Tag("py3", "none", "any"), From 6d4aea168ce4fb62d7db953632d067b983e170f9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Fri, 24 Jan 2020 16:20:47 +0530 Subject: [PATCH 085/114] Update CHANGELOG for 20.1 --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3241b8fe5..884bdfeca 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,10 +1,10 @@ Changelog --------- -20.1 - *unreleased* +20.1 - 2020-01-24 ~~~~~~~~~~~~~~~~~~~ -No changes yet. +* Fix a bug caused by reuse of an exhausted iterator. (:issue:`257`) 20.0 - 2020-01-06 ~~~~~~~~~~~~~~~~~ From 71b46f5a4ad9275b67c4b4eae873930f2d604aa9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Fri, 24 Jan 2020 16:22:37 +0530 Subject: [PATCH 086/114] Bump for release --- packaging/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/__about__.py b/packaging/__about__.py index e72680ad4..08d2c892b 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.1.dev0" +__version__ = "20.1" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From 93f1df24aec50453bf2a85e5e7b443a8adae54aa Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 27 Jan 2020 16:42:57 +0530 Subject: [PATCH 087/114] Switch from tox to nox (#255) --- .github/workflows/docs.yml | 6 +-- .github/workflows/lint.yml | 8 ++-- .gitignore | 2 +- .travis.yml | 25 +++++----- MANIFEST.in | 2 +- docs/development/getting-started.rst | 45 ++++++++++-------- docs/development/submitting-patches.rst | 2 +- noxfile.py | 61 +++++++++++++++++++++++++ tox.ini | 43 ----------------- 9 files changed, 111 insertions(+), 83 deletions(-) create mode 100644 noxfile.py delete mode 100644 tox.ini diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e8c579536..342b4c18d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ on: jobs: docs: - name: tox -e docs + name: nox -s docs runs-on: ubuntu-latest steps: @@ -24,7 +24,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade tox + python -m pip install --upgrade nox - name: Build documentation - run: python -m tox -e docs + run: python -m nox -s docs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8dd3217cc..6c1715815 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ on: jobs: lint: - name: tox -e lint + name: nox -s lint runs-on: ubuntu-latest steps: @@ -24,10 +24,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade tox + python -m pip install --upgrade nox - - name: Run `tox -e lint` - run: python -m tox -e lint + - name: Run `nox -s lint` + run: python -m nox -s lint build: name: Build sdist and wheel diff --git a/.gitignore b/.gitignore index 8250df26f..e0f870219 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.egg *.py[co] -.tox/ +.[nt]ox/ .cache/ .coverage .idea diff --git a/.travis.yml b/.travis.yml index bfa253d41..7f0973ae2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,29 +5,30 @@ python: 3.8 matrix: include: - python: 2.7 - env: TOXENV=py27 + env: NOXSESSION=tests-2.7 - python: pypy - env: TOXENV=pypy + env: NOXSESSION=tests-pypy - python: pypy3 - env: TOXENV=pypy3 + env: NOXSESSION=tests-pypy3 - python: 3.4 - env: TOXENV=py34 + env: NOXSESSION=tests-3.4 - python: 3.5 - env: TOXENV=py35 + env: NOXSESSION=tests-3.5 - python: 3.6 - env: TOXENV=py36 + env: NOXSESSION=tests-3.6 - python: 3.7 - env: TOXENV=py37 + env: NOXSESSION=tests-3.7 - python: 3.8 - env: TOXENV=py38 - - env: TOXENV=lint - - env: TOXENV=docs + env: NOXSESSION=tests-3.8 + - env: NOXSESSION=lint + - env: NOXSESSION=docs install: - - pip install tox + - pyenv global 3.7.1 + - python3.7 -m pip install nox script: - - tox + - python3.7 -m nox notifications: irc: diff --git a/MANIFEST.in b/MANIFEST.in index 19afb627c..468f5b7e3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,12 +4,12 @@ include LICENSE LICENSE.APACHE LICENSE.BSD include .coveragerc include .flake8 include .pre-commit-config.yaml -include tox.ini recursive-include docs * recursive-include tests *.py recursive-include tests hello-world-* +exclude noxfile.py exclude .travis.yml exclude dev-requirements.txt exclude tests/build-hello-world.sh diff --git a/docs/development/getting-started.rst b/docs/development/getting-started.rst index 75a1d8014..c8e948782 100644 --- a/docs/development/getting-started.rst +++ b/docs/development/getting-started.rst @@ -3,7 +3,7 @@ Getting started Working on packaging requires the installation of a small number of development dependencies. To see what dependencies are required to -run the tests manually, please look at the ``tox.ini`` file. +run the tests manually, please look at the ``noxfile.py`` file. Running tests ~~~~~~~~~~~~~ @@ -16,33 +16,43 @@ automatically, so all you have to do is: $ python -m pytest ... - 62746 passed in 220.43 seconds + 29204 passed, 4 skipped, 1 xfailed in 83.98 seconds This runs the tests with the default Python interpreter. This also allows you to run select tests instead of the entire test suite. You can also verify that the tests pass on other supported Python interpreters. -For this we use `tox`_, which will automatically create a `virtualenv`_ for +For this we use `nox`_, which will automatically create a `virtualenv`_ for each supported Python version and run the tests. For example: .. code-block:: console - $ tox + $ nox -s tests ... - py27: commands succeeded - ERROR: pypy: InterpreterNotFound: pypy - ERROR: py34: InterpreterNotFound: python3.4 - ERROR: py35: InterpreterNotFound: python3.5 - py36: commands succeeded - ERROR: py37: InterpreterNotFound: python3.7 - docs: commands succeeded - pep8: commands succeeded + nox > Ran multiple sessions: + nox > * tests-2.7: success + nox > * tests-3.4: skipped + nox > * tests-3.5: success + nox > * tests-3.6: success + nox > * tests-3.7: success + nox > * tests-3.8: success + nox > * tests-pypy: skipped + nox > * tests-pypy3: skipped You may not have all the required Python versions installed, in which case you will see one or more ``InterpreterNotFound`` errors. -If you wish to run just the linting rules, you may use `pre-commit`_. +Running linters +~~~~~~~~~~~~~~~ +If you wish to run the linting rules, you may use `pre-commit`_ or run +``nox -s lint``. + +.. code-block:: console + + $ nox -s lint + ... + nox > Session lint was successful. Building documentation ~~~~~~~~~~~~~~~~~~~~~~ @@ -50,20 +60,19 @@ Building documentation packaging documentation is stored in the ``docs/`` directory. It is written in `reStructured Text`_ and rendered using `Sphinx`_. -Use `tox`_ to build the documentation. For example: +Use `nox`_ to build the documentation. For example: .. code-block:: console - $ tox -e docs + $ nox -s docs ... - docs: commands succeeded - congratulations :) + nox > Session docs was successful. The HTML documentation index can now be found at ``docs/_build/html/index.html``. .. _`pytest`: https://pypi.org/project/pytest/ -.. _`tox`: https://pypi.org/project/tox/ +.. _`nox`: https://pypi.org/project/nox/ .. _`virtualenv`: https://pypi.org/project/virtualenv/ .. _`pip`: https://pypi.org/project/pip/ .. _`sphinx`: https://pypi.org/project/Sphinx/ diff --git a/docs/development/submitting-patches.rst b/docs/development/submitting-patches.rst index f139c778b..cf433c45f 100644 --- a/docs/development/submitting-patches.rst +++ b/docs/development/submitting-patches.rst @@ -19,7 +19,7 @@ Code ---- This project's source is auto-formatted with |black|. You can check if your -code meets our requirements by running our linters against it with ``tox -e +code meets our requirements by running our linters against it with ``nox -s lint`` or ``pre-commit run --all-files``. `Write comments as complete sentences.`_ diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000..e85ef43a1 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,61 @@ +# mypy: disallow-untyped-defs=False, disallow-untyped-calls=False + +import glob +import shutil + +import nox + +nox.options.sessions = ["lint"] +nox.options.reuse_existing_virtualenvs = True + + +@nox.session(python=["2.7", "3.4", "3.5", "3.6", "3.7", "3.8", "pypy", "pypy3"]) +def tests(session): + def coverage(*args): + session.run("python", "-m", "coverage", *args) + + session.install("coverage<5.0.0", "pretend", "pytest", "pip>=9.0.2") + + if "pypy" not in session.python: + coverage("run", "--source", "packaging/", "-m", "pytest", "--strict") + coverage("report", "-m", "--fail-under", "100") + else: + # Don't do coverage tracking for PyPy, since it's SLOW. + session.run("pytest", "--capture=no", "--strict", *session.posargs) + + +@nox.session(python="3.8") +def lint(session): + # Run the linters (via pre-commit) + session.install("pre-commit") + session.run("pre-commit", "run", "--all-files") + + # Check the distribution + session.install("setuptools", "readme_renderer", "twine", "wheel") + session.run("python", "setup.py", "--quiet", "sdist", "bdist_wheel") + session.run("twine", "check", *glob.glob("dist/*")) + + +@nox.session(python="3.8") +def docs(session): + shutil.rmtree("docs/_build", ignore_errors=True) + session.install("sphinx", "sphinx-rtd-theme") + + variants = [ + # (builder, dest) + ("html", "html"), + ("latex", "latex"), + ("doctest", "html"), + ] + + for builder, dest in variants: + session.run( + "sphinx-build", + "-W", + "-b", + builder, + "-d", + "docs/_build/doctrees/" + dest, + "docs", # source directory + "docs/_build/" + dest, # output directory + ) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index c3c34f18c..000000000 --- a/tox.ini +++ /dev/null @@ -1,43 +0,0 @@ -[tox] -envlist = py27,pypy,pypy3,py34,py35,py36,py37,py38,docs,lint - -[testenv] -deps = - coverage<5.0.0 - pretend - pytest - pip>=9.0.2 -commands = - python -m coverage run --source packaging/ -m pytest --strict {posargs} tests - python -m coverage report -m --fail-under 100 - -[testenv:pypy] -commands = - pytest --capture=no --strict {posargs} tests - -[testenv:pypy3] -commands = - pytest --capture=no --strict {posargs} tests - -[testenv:lint] -basepython=python3 -deps = - pre-commit - readme_renderer - setuptools - twine - wheel -commands = - pre-commit run --all-files - python setup.py --quiet sdist bdist_wheel - twine check dist/* - -[testenv:docs] -basepython = python3 -deps = - sphinx - sphinx_rtd_theme -commands = - sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html - sphinx-build -W -b latex -d {envtmpdir}/doctrees docs docs/_build/latex - sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html From 54d57d7865d916f7d86fa03cf79e0d7a1803ad05 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 28 Jan 2020 02:14:11 +0530 Subject: [PATCH 088/114] Update CHANGELOG to reflect unreleased changes (#266) --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 884bdfeca..58aa51a77 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog --------- +*unreleased* +~~~~~~~~~~~~ + +No changes yet. + 20.1 - 2020-01-24 ~~~~~~~~~~~~~~~~~~~ From 939b28b19087a4222bf021f952ef7c1c88ebb50c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 28 Jan 2020 11:41:58 +0530 Subject: [PATCH 089/114] Automated releases - Round 1 (#265) First shot at automating the release process --- .pre-commit-config.yaml | 2 +- docs/development/release-process.rst | 39 +++---- noxfile.py | 156 ++++++++++++++++++++++++++- 3 files changed, 171 insertions(+), 26 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7b208c9f..691246784 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: args: [] - id: mypy name: mypy for Python 2 - exclude: '^(docs|tasks|tests)|setup\.py' + exclude: '^(docs|tasks|tests)|setup\.py|noxfile\.py' args: ['--py2'] - repo: https://github.com/psf/black diff --git a/docs/development/release-process.rst b/docs/development/release-process.rst index 11c61aabd..6bf11b22c 100644 --- a/docs/development/release-process.rst +++ b/docs/development/release-process.rst @@ -1,35 +1,26 @@ Release Process =============== -#. Checkout the current ``master`` branch, with a clean working directory. -#. Modify the ``CHANGELOG.rst`` to include changes made since the last release - and update the section header for the new release. -#. Bump the version in ``packaging/__about__.py`` - -#. Install the latest ``setuptools``, ``wheel`` and ``twine`` packages - from PyPI:: - - $ pip install --upgrade setuptools wheel twine - -#. Ensure no ``dist/`` folder exists and then create the distribution files:: +#. Checkout the current ``master`` branch. +#. Install the latest ``nox``:: - $ python setup.py sdist bdist_wheel + $ pip install nox -#. Check the built distribution files with ``twine``:: - - $ twine check dist/* +#. Modify the ``CHANGELOG.rst`` to include changes made since the last release + and update the section header for the new release. -#. Commit the changes to ``master``. +#. Run the release automation with the required version number (YY.N):: -#. If all goes well, upload the build distribution files:: + $ nox -s release -- YY.N - $ twine upload dist/* +#. Modify the ``CHANGELOG.rst`` to reflect the development version does not + have any changes since the last release. -#. Create a - `release on GitHub `_ and - include the artifacts uploaded to PyPI. +#. Notify the other project owners of the release. -#. Bump the version for development in ``packaging/__about__.py`` and - ``CHANGELOG.rst``. +.. note:: + Access needed for making the release are: -#. Notify the other project owners of the release. + - PyPI maintainer (or owner) access to `packaging` + - push directly to the `master` branch on the source repository + - push tags directly to the source repository diff --git a/noxfile.py b/noxfile.py index e85ef43a1..80446d541 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,7 +1,13 @@ # mypy: disallow-untyped-defs=False, disallow-untyped-calls=False +import time +import re +import os +import sys import glob import shutil +import subprocess +from pathlib import Path import nox @@ -31,7 +37,7 @@ def lint(session): session.run("pre-commit", "run", "--all-files") # Check the distribution - session.install("setuptools", "readme_renderer", "twine", "wheel") + session.install("setuptools", "twine", "wheel") session.run("python", "setup.py", "--quiet", "sdist", "bdist_wheel") session.run("twine", "check", *glob.glob("dist/*")) @@ -59,3 +65,151 @@ def docs(session): "docs", # source directory "docs/_build/" + dest, # output directory ) + + +@nox.session +def release(session): + package_name = "packaging" + version_file = Path(f"{package_name}/__about__.py") + + try: + release_version = _get_version_from_arguments(session.posargs) + except ValueError as e: + session.error(f"Invalid arguments: {e}") + + # Check state of working directory and git. + _check_working_directory_state(session) + _check_git_state(session, release_version) + + # Update to the release version. + _bump(session, version=release_version, file=version_file, kind="release") + + # Tag the release commit. + session.run("git", "tag", "-s", release_version, external=True) + + # Bump the version for development. + major, minor = map(int, release_version.split(".")) + next_version = f"{major}.{minor + 1}.dev0" + _bump(session, version=next_version, file=version_file, kind="development") + + # Checkout the git tag. + session.run("git", "checkout", "-q", release_version, external=True) + + # Build the distribution. + session.run("python", "setup.py", "sdist", "bdist_wheel") + + # Check what files are in dist/ for upload. + files = glob.glob(f"dist/*") + assert sorted(files) == [ + f"dist/{package_name}-{release_version}.tag.gz", + f"dist/{package_name}-{release_version}-py2.py3-none-any.whl", + ], f"Got the wrong files: {files}" + + # Get back out into master. + session.run("git", "checkout", "-q", "master", external=True) + + # Check and upload distribution files. + session.run("twine", "check", *files) + + # Upload the distribution. + session.run("twine", "upload", *files) + + # Push the commits and tag. + # NOTE: The following fails if pushing to the branch is not allowed. This can + # happen on GitHub, if the master branch is protected, there are required + # CI checks and "Include administrators" is enabled on the protection. + session.run("git", "push", "upstream", "master", release_version, external=True) + + +# ----------------------------------------------------------------------------- +# Helpers +# ----------------------------------------------------------------------------- +def _get_version_from_arguments(arguments): + """Checks the arguments passed to `nox -s release`. + + Only 1 argument that looks like a version? Return the argument. + Otherwise, raise a ValueError describing what's wrong. + """ + if len(arguments) != 1: + raise ValueError("Expected exactly 1 argument") + + version = arguments[0] + parts = version.split(".") + + if len(parts) != 2: + # Not of the form: YY.N + raise ValueError("not of the form: YY.N") + + if not all(part.isdigit() for part in parts): + # Not all segments are integers. + raise ValueError("non-integer segments") + + # All is good. + return version + + +def _check_working_directory_state(session): + """Check state of the working directory, prior to making the release. + """ + should_not_exist = ["build/", "dist/"] + + bad_existing_paths = list(filter(os.path.exists, should_not_exist)) + if bad_existing_paths: + session.error(f"Remove {', '.join(bad_existing_paths)} and try again") + + +def _check_git_state(session, version_tag): + """Check state of the git repository, prior to making the release. + """ + # Ensure the upstream remote pushes to the correct URL. + allowed_upstreams = [ + "git@github.com:pypa/packaging.git", + "https://github.com/pypa/packaging.git", + ] + result = subprocess.run( + ["git", "remote", "get-url", "--push", "upstream"], + capture_output=True, + encoding="utf-8", + ) + if result.stdout.rstrip() not in allowed_upstreams: + session.error(f"git remote `upstream` is not one of {allowed_upstreams}") + # Ensure we're on master branch for cutting a release. + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, + encoding="utf-8", + ) + if result.stdout != "master\n": + session.error(f"Not on master branch: {result.stdout!r}") + + # Ensure there are no uncommitted changes. + result = subprocess.run( + ["git", "status", "--porcelain"], capture_output=True, encoding="utf-8" + ) + if result.stdout: + print(result.stdout, end="", file=sys.stderr) + session.error(f"The working tree has uncommitted changes") + + # Ensure this tag doesn't exist already. + result = subprocess.run( + ["git", "rev-parse", version_tag], capture_output=True, encoding="utf-8" + ) + if not result.returncode: + session.error(f"Tag already exists! {version_tag} -- {result.stdout!r}") + + # Back up the current git reference, in a tag that's easy to clean up. + _release_backup_tag = "auto/release-start-" + str(int(time.time())) + session.run("git", "tag", _release_backup_tag, external=True) + + +def _bump(session, *, version, file, kind): + session.log(f"Bump version to {version!r}") + contents = file.read_text() + new_contents = re.sub( + '__version__ = "(.+)"', f'__version__ = "{version}"', contents + ) + file.write_text(new_contents) + + session.log(f"git commit") + subprocess.run(["git", "add", str(file)]) + subprocess.run(["git", "commit", "-m", f"Bump for {kind}"]) From ed9c96c12bc6c8919e6a64f5c6164a6534ce3c6b Mon Sep 17 00:00:00 2001 From: Hong Xu Date: Wed, 29 Jan 2020 02:27:22 -0800 Subject: [PATCH 090/114] Call pytest using "python -m pytest" (#267) Co-authored-by: Brett Cannon <54418+brettcannon@users.noreply.github.com> --- noxfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 80446d541..2bcdf5146 100644 --- a/noxfile.py +++ b/noxfile.py @@ -27,7 +27,9 @@ def coverage(*args): coverage("report", "-m", "--fail-under", "100") else: # Don't do coverage tracking for PyPy, since it's SLOW. - session.run("pytest", "--capture=no", "--strict", *session.posargs) + session.run( + "python", "-m", "pytest", "--capture=no", "--strict", *session.posargs + ) @nox.session(python="3.8") From f1d67cb7b6903c29c725fe46777d6e28757e04d5 Mon Sep 17 00:00:00 2001 From: Marius Bakke Date: Tue, 4 Feb 2020 09:29:23 +0100 Subject: [PATCH 091/114] Fix test_linux_platforms_manylinux* on architectures other than x86_64 (#256) * Fix test_linux_platforms_manylinux tests for i686. By not assuming the platform is x86_64. Fixes #254. * Allow test_linux_platforms_manylinux tests to pass on non x86. The expected output only occurs on x86 platforms, so mock it on other architectures. Co-authored-by: Brett Cannon <54418+brettcannon@users.noreply.github.com> --- tests/test_tags.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/test_tags.py b/tests/test_tags.py index 1c05969f7..d7c1514a2 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -435,37 +435,43 @@ def test_linux_platforms_manylinux_unsupported(self, monkeypatch): linux_platform = list(tags._linux_platforms(is_32bit=False)) assert linux_platform == ["linux_x86_64"] - def test_linux_platforms_manylinux1(self, monkeypatch): + def test_linux_platforms_manylinux1(self, is_x86, monkeypatch): monkeypatch.setattr( tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux1" ) - if platform.system() != "Linux": + if platform.system() != "Linux" or not is_x86: monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(platform, "machine", lambda: "x86_64") platforms = list(tags._linux_platforms(is_32bit=False)) - assert platforms == ["manylinux1_x86_64", "linux_x86_64"] + arch = platform.machine() + assert platforms == ["manylinux1_" + arch, "linux_" + arch] - def test_linux_platforms_manylinux2010(self, monkeypatch): + def test_linux_platforms_manylinux2010(self, is_x86, monkeypatch): monkeypatch.setattr( tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2010" ) - if platform.system() != "Linux": + if platform.system() != "Linux" or not is_x86: monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(platform, "machine", lambda: "x86_64") platforms = list(tags._linux_platforms(is_32bit=False)) - expected = ["manylinux2010_x86_64", "manylinux1_x86_64", "linux_x86_64"] + arch = platform.machine() + expected = ["manylinux2010_" + arch, "manylinux1_" + arch, "linux_" + arch] assert platforms == expected - def test_linux_platforms_manylinux2014(self, monkeypatch): + def test_linux_platforms_manylinux2014(self, is_x86, monkeypatch): monkeypatch.setattr( tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2014" ) - if platform.system() != "Linux": + if platform.system() != "Linux" or not is_x86: monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(platform, "machine", lambda: "x86_64") platforms = list(tags._linux_platforms(is_32bit=False)) + arch = platform.machine() expected = [ - "manylinux2014_x86_64", - "manylinux2010_x86_64", - "manylinux1_x86_64", - "linux_x86_64", + "manylinux2014_" + arch, + "manylinux2010_" + arch, + "manylinux1_" + arch, + "linux_" + arch, ] assert platforms == expected From 64d7d16d2a30aa31e0ae94b8fffa3673e6ad256c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Wed, 5 Feb 2020 15:26:50 +0530 Subject: [PATCH 092/114] Modify test GitHub Action to use nox (#269) * Use pypy2 instead of pypy for test session name * Update Travis CI, for pypy -> pypy2 name change * Add initial variant of fully-nox GitHub Actions CI * Use Python 3.x, when on Python <3.6 * Do not fail fast * Exclude Windows + PyPy3 from GitHub Actions' tests Includes a link to the exact failing check, so that someone debugging this later can find context more easily. * Factor out the common "-latest" suffix * Use runner.os instead of matrix.os * Address review comments --- .github/workflows/test.yml | 68 ++++++++++++++++---------------------- .travis.yml | 2 +- noxfile.py | 2 +- 3 files changed, 31 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7972dcddd..532c11ea6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,16 +11,19 @@ on: - '**.py' jobs: - test-cpython: - name: Test CPython ${{ matrix.python_version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - + test: + runs-on: ${{ matrix.os }}-latest strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + os: [ubuntu, windows, macOS] # Python 3.4 is not available from actions/setup-python@v1. - python_version: ['2.7', '3.5', '3.6', '3.7', '3.8'] + python_version: ['2.7', '3.5', '3.6', '3.7', '3.8', 'pypy2', 'pypy3'] + exclude: + # This is failing due to pip not being in the virtual environment. + # https://github.com/pypa/packaging/runs/424785871#step:7:9 + - os: windows + python_version: pypy3 steps: - uses: actions/checkout@v1 @@ -30,46 +33,33 @@ jobs: with: python-version: ${{ matrix.python_version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install 'coverage<5.0.0' pretend pytest - shell: bash - - - name: Test coverage - run: | - python -m coverage run --source packaging/ -m pytest --strict tests - python -m coverage report -m --fail-under 100 - shell: bash - - - test-pypy: - name: Test ${{ matrix.pypy_version }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] - pypy_version: ['pypy2', 'pypy3'] - - steps: - - uses: actions/checkout@v1 - + # Set `python` to a recent 3.x version if we're not testing Python 3.6+. + # Why? Nox needs Python 3.5+ and everyone likes f-strings. - uses: actions/setup-python@v1 - name: Install ${{ matrix.pypy_version }} + name: Install Python 3.x with: - python-version: ${{ matrix.pypy_version }} + python-version: 3.x + if: > + ( + matrix.python_version == '2.7' || + matrix.python_version == 'pypy2' || + matrix.python_version == '3.5' + ) + + # Workaround https://github.com/theacodes/nox/issues/250 + - name: Workaround for Windows Python 2.7 + # This is in PATH, so nox resolves to it - but then subsequent steps fail. + run: rm C:/ProgramData/Chocolatey/bin/python2.7.exe + shell: bash + if: runner.os == 'Windows' && matrix.python_version == '2.7' - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install 'coverage<5.0.0' pretend pytest + python -m pip install nox shell: bash - - name: Test coverage + - name: Run nox run: | - python -m pytest --capture=no --strict - python -m coverage run --source packaging/ -m pytest --strict tests - python -m coverage report -m --fail-under 100 + python -m nox -s tests-${{ matrix.python_version }} shell: bash diff --git a/.travis.yml b/.travis.yml index 7f0973ae2..b60b6f12a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ matrix: - python: 2.7 env: NOXSESSION=tests-2.7 - python: pypy - env: NOXSESSION=tests-pypy + env: NOXSESSION=tests-pypy2 - python: pypy3 env: NOXSESSION=tests-pypy3 - python: 3.4 diff --git a/noxfile.py b/noxfile.py index 2bcdf5146..f7a512bb6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -15,7 +15,7 @@ nox.options.reuse_existing_virtualenvs = True -@nox.session(python=["2.7", "3.4", "3.5", "3.6", "3.7", "3.8", "pypy", "pypy3"]) +@nox.session(python=["2.7", "3.4", "3.5", "3.6", "3.7", "3.8", "pypy2", "pypy3"]) def tests(session): def coverage(*args): session.run("python", "-m", "coverage", *args) From 46bedf3d307e22901c71e3c03a9f29a0deceb202 Mon Sep 17 00:00:00 2001 From: Hong Xu Date: Thu, 6 Feb 2020 11:02:55 -0800 Subject: [PATCH 093/114] 32-bit Linux running on 64-bit ARM arch should have platform "linux_armv7l" (#234) --- CHANGELOG.rst | 3 ++- packaging/tags.py | 7 +++++-- tests/test_tags.py | 34 ++++++++++++++++------------------ 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 58aa51a77..a56d64638 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,8 @@ Changelog *unreleased* ~~~~~~~~~~~~ -No changes yet. +* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8, + aarch64), to report the wrong bitness. 20.1 - 2020-01-24 ~~~~~~~~~~~~~~~~~~~ diff --git a/packaging/tags.py b/packaging/tags.py index 60a69d8f9..3c5e11a51 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -636,8 +636,11 @@ def _have_compatible_manylinux_abi(arch): 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" + 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): diff --git a/tests/test_tags.py b/tests/test_tags.py index d7c1514a2..82364898d 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -37,11 +37,6 @@ def is_x86(): return re.match(r"(i\d86|x86_64)", platform.machine()) is not None -@pytest.fixture -def is_64bit_os(): - return platform.architecture()[0] == "64bit" - - @pytest.fixture def manylinux_module(monkeypatch): monkeypatch.setattr(tags, "_have_compatible_glibc", lambda *args: False) @@ -415,19 +410,22 @@ def test_glibc_version_string_none(self, monkeypatch): monkeypatch.setattr(tags, "_glibc_version_string", lambda: None) assert not tags._have_compatible_glibc(2, 4) - def test_linux_platforms_64bit_on_64bit_os(self, is_64bit_os, is_x86, monkeypatch): - if platform.system() != "Linux" or not is_64bit_os or not is_x86: - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) - linux_platform = list(tags._linux_platforms(is_32bit=False))[-1] - assert linux_platform == "linux_x86_64" - - def test_linux_platforms_32bit_on_64bit_os(self, is_64bit_os, is_x86, monkeypatch): - if platform.system() != "Linux" or not is_64bit_os or not is_x86: - monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") - monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) - linux_platform = list(tags._linux_platforms(is_32bit=True))[-1] - assert linux_platform == "linux_i686" + @pytest.mark.parametrize( + "arch,is_32bit,expected", + [ + ("linux-x86_64", False, "linux_x86_64"), + ("linux-x86_64", True, "linux_i686"), + ("linux-aarch64", False, "linux_aarch64"), + ("linux-aarch64", True, "linux_armv7l"), + ], + ) + def test_linux_platforms_32_64bit_on_64bit_os( + self, arch, is_32bit, expected, monkeypatch + ): + monkeypatch.setattr(distutils.util, "get_platform", lambda: arch) + monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) + linux_platform = list(tags._linux_platforms(is_32bit=is_32bit))[-1] + assert linux_platform == expected def test_linux_platforms_manylinux_unsupported(self, monkeypatch): monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") From 318a0347665fb0990ed33f00e0434615ece5ea3a Mon Sep 17 00:00:00 2001 From: "John T. Wodder II" Date: Fri, 7 Feb 2020 19:05:07 -0500 Subject: [PATCH 094/114] tags: Separate versions with multi-digit components with '_' (#240) --- packaging/tags.py | 26 ++++--- tests/test_tags.py | 173 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 168 insertions(+), 31 deletions(-) diff --git a/packaging/tags.py b/packaging/tags.py index 3c5e11a51..300faab84 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -162,7 +162,7 @@ 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 = _get_config_var("Py_DEBUG", warn) has_refcount = hasattr(sys, "gettotalrefcount") @@ -221,10 +221,7 @@ def cpython_tags( if not python_version: python_version = sys.version_info[:2] - if len(python_version) < 2: - interpreter = "cp{}".format(python_version[0]) - else: - interpreter = "cp{}{}".format(*python_version[:2]) + interpreter = "cp{}".format(_version_nodot(python_version[:2])) if abis is None: if len(python_version) > 1: @@ -252,8 +249,8 @@ def cpython_tags( if _abi3_applies(python_version): for minor_version in range(python_version[1] - 1, 1, -1): for platform_ in platforms: - interpreter = "cp{major}{minor}".format( - major=python_version[0], minor=minor_version + interpreter = "cp{version}".format( + version=_version_nodot((python_version[0], minor_version)) ) yield Tag(interpreter, "abi3", platform_) @@ -305,11 +302,11 @@ def _py_interpreter_range(py_version): all previous versions of that major version. """ if len(py_version) > 1: - yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) + yield "py{version}".format(version=_version_nodot(py_version[:2])) yield "py{major}".format(major=py_version[0]) if len(py_version) > 1: for minor in range(py_version[1] - 1, -1, -1): - yield "py{major}{minor}".format(major=py_version[0], minor=minor) + yield "py{version}".format(version=_version_nodot((py_version[0], minor))) def compatible_tags( @@ -707,10 +704,19 @@ def interpreter_version(**kwargs): if version: version = str(version) else: - version = "".join(map(str, sys.version_info[:2])) + 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(**kwargs): # type: (bool) -> Iterator[Tag] """ diff --git a/tests/test_tags.py b/tests/test_tags.py index 82364898d..4840e196d 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -191,10 +191,20 @@ def test_python_version_nodot(self, monkeypatch): monkeypatch.setattr(tags, "_get_config_var", lambda var, warn: "NN") assert tags.interpreter_version() == "NN" - def test_sys_version_info(self, monkeypatch): + @pytest.mark.parametrize( + "version_info,version_str", + [ + ((1, 2, 3), "12"), + ((1, 12, 3), "1_12"), + ((11, 2, 3), "11_2"), + ((11, 12, 3), "11_12"), + ((1, 2, 13), "12"), + ], + ) + def test_sys_version_info(self, version_info, version_str, monkeypatch): monkeypatch.setattr(tags, "_get_config_var", lambda *args, **kwargs: None) - monkeypatch.setattr(sys, "version_info", ("L", "M", "N")) - assert tags.interpreter_version() == "LM" + monkeypatch.setattr(sys, "version_info", version_info) + assert tags.interpreter_version() == version_str class TestMacOSPlatforms: @@ -691,7 +701,7 @@ def test_wide_unicode(self, unicode_size, maxunicode, version, result, monkeypat config = {"Py_DEBUG": 0, "WITH_PYMALLOC": 0, "Py_UNICODE_SIZE": unicode_size} monkeypatch.setattr(sysconfig, "get_config_var", config.__getitem__) monkeypatch.setattr(sys, "maxunicode", maxunicode) - base_abi = "cp{}{}".format(version[0], version[1]) + base_abi = "cp" + tags._version_nodot(version) expected = [base_abi + "u" if result else base_abi] assert tags._cpython_abis(version) == expected @@ -701,9 +711,41 @@ def test_iterator_returned(self): result_iterator = tags.cpython_tags( (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"] ) - isinstance(result_iterator, collections_abc.Iterator) + assert isinstance(result_iterator, collections_abc.Iterator) def test_all_args(self): + result_iterator = tags.cpython_tags( + (3, 11), ["cp3_11d", "cp3_11"], ["plat1", "plat2"] + ) + result = list(result_iterator) + assert result == [ + tags.Tag("cp3_11", "cp3_11d", "plat1"), + tags.Tag("cp3_11", "cp3_11d", "plat2"), + tags.Tag("cp3_11", "cp3_11", "plat1"), + tags.Tag("cp3_11", "cp3_11", "plat2"), + tags.Tag("cp3_11", "abi3", "plat1"), + tags.Tag("cp3_11", "abi3", "plat2"), + tags.Tag("cp3_11", "none", "plat1"), + tags.Tag("cp3_11", "none", "plat2"), + tags.Tag("cp3_10", "abi3", "plat1"), + tags.Tag("cp3_10", "abi3", "plat2"), + tags.Tag("cp39", "abi3", "plat1"), + tags.Tag("cp39", "abi3", "plat2"), + tags.Tag("cp38", "abi3", "plat1"), + tags.Tag("cp38", "abi3", "plat2"), + tags.Tag("cp37", "abi3", "plat1"), + tags.Tag("cp37", "abi3", "plat2"), + tags.Tag("cp36", "abi3", "plat1"), + tags.Tag("cp36", "abi3", "plat2"), + tags.Tag("cp35", "abi3", "plat1"), + tags.Tag("cp35", "abi3", "plat2"), + tags.Tag("cp34", "abi3", "plat1"), + tags.Tag("cp34", "abi3", "plat2"), + tags.Tag("cp33", "abi3", "plat1"), + tags.Tag("cp33", "abi3", "plat2"), + tags.Tag("cp32", "abi3", "plat1"), + tags.Tag("cp32", "abi3", "plat2"), + ] result_iterator = tags.cpython_tags( (3, 8), ["cp38d", "cp38"], ["plat1", "plat2"] ) @@ -730,6 +772,7 @@ def test_all_args(self): tags.Tag("cp32", "abi3", "plat1"), tags.Tag("cp32", "abi3", "plat2"), ] + result = list(tags.cpython_tags((3, 3), ["cp33m"], ["plat1", "plat2"])) assert result == [ tags.Tag("cp33", "cp33m", "plat1"), @@ -744,7 +787,7 @@ def test_all_args(self): def test_python_version_defaults(self): tag = next(tags.cpython_tags(abis=["abi3"], platforms=["any"])) - interpreter = "cp{}{}".format(*sys.version_info[:2]) + interpreter = "cp" + tags._version_nodot(sys.version_info[:2]) assert interpreter == tag.interpreter def test_abi_defaults(self, monkeypatch): @@ -754,11 +797,23 @@ def test_abi_defaults(self, monkeypatch): assert tags.Tag("cp38", "abi3", "any") in result assert tags.Tag("cp38", "none", "any") in result + def test_abi_defaults_needs_underscore(self, monkeypatch): + monkeypatch.setattr(tags, "_cpython_abis", lambda _1, _2: ["cp3_11"]) + result = list(tags.cpython_tags((3, 11), platforms=["any"])) + assert tags.Tag("cp3_11", "cp3_11", "any") in result + assert tags.Tag("cp3_11", "abi3", "any") in result + assert tags.Tag("cp3_11", "none", "any") in result + def test_platforms_defaults(self, monkeypatch): monkeypatch.setattr(tags, "_platform_tags", lambda: ["plat1"]) result = list(tags.cpython_tags((3, 8), abis=["whatever"])) assert tags.Tag("cp38", "whatever", "plat1") in result + def test_platforms_defaults_needs_underscore(self, monkeypatch): + monkeypatch.setattr(tags, "_platform_tags", lambda: ["plat1"]) + result = list(tags.cpython_tags((3, 11), abis=["whatever"])) + assert tags.Tag("cp3_11", "whatever", "plat1") in result + def test_major_only_python_version(self): result = list(tags.cpython_tags((3,), ["abi"], ["plat"])) assert result == [ @@ -894,6 +949,51 @@ def test_all_args(self): tags.Tag("py30", "none", "any"), ] + def test_all_args_needs_underscore(self): + result = list(tags.compatible_tags((3, 11), "cp3_11", ["plat1", "plat2"])) + assert result == [ + tags.Tag("py3_11", "none", "plat1"), + tags.Tag("py3_11", "none", "plat2"), + tags.Tag("py3", "none", "plat1"), + tags.Tag("py3", "none", "plat2"), + tags.Tag("py3_10", "none", "plat1"), + tags.Tag("py3_10", "none", "plat2"), + tags.Tag("py39", "none", "plat1"), + tags.Tag("py39", "none", "plat2"), + tags.Tag("py38", "none", "plat1"), + tags.Tag("py38", "none", "plat2"), + tags.Tag("py37", "none", "plat1"), + tags.Tag("py37", "none", "plat2"), + tags.Tag("py36", "none", "plat1"), + tags.Tag("py36", "none", "plat2"), + tags.Tag("py35", "none", "plat1"), + tags.Tag("py35", "none", "plat2"), + tags.Tag("py34", "none", "plat1"), + tags.Tag("py34", "none", "plat2"), + tags.Tag("py33", "none", "plat1"), + tags.Tag("py33", "none", "plat2"), + tags.Tag("py32", "none", "plat1"), + tags.Tag("py32", "none", "plat2"), + tags.Tag("py31", "none", "plat1"), + tags.Tag("py31", "none", "plat2"), + tags.Tag("py30", "none", "plat1"), + tags.Tag("py30", "none", "plat2"), + tags.Tag("cp3_11", "none", "any"), + tags.Tag("py3_11", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py3_10", "none", "any"), + tags.Tag("py39", "none", "any"), + tags.Tag("py38", "none", "any"), + tags.Tag("py37", "none", "any"), + tags.Tag("py36", "none", "any"), + tags.Tag("py35", "none", "any"), + tags.Tag("py34", "none", "any"), + tags.Tag("py33", "none", "any"), + tags.Tag("py32", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + def test_major_only_python_version(self): result = list(tags.compatible_tags((3,), "cp33", ["plat"])) assert result == [ @@ -915,6 +1015,39 @@ def test_default_python_version(self, monkeypatch): tags.Tag("py30", "none", "any"), ] + def test_default_python_version_needs_underscore(self, monkeypatch): + monkeypatch.setattr(sys, "version_info", (3, 11)) + result = list(tags.compatible_tags(interpreter="cp3_11", platforms=["plat"])) + assert result == [ + tags.Tag("py3_11", "none", "plat"), + tags.Tag("py3", "none", "plat"), + tags.Tag("py3_10", "none", "plat"), + tags.Tag("py39", "none", "plat"), + tags.Tag("py38", "none", "plat"), + tags.Tag("py37", "none", "plat"), + tags.Tag("py36", "none", "plat"), + tags.Tag("py35", "none", "plat"), + tags.Tag("py34", "none", "plat"), + tags.Tag("py33", "none", "plat"), + tags.Tag("py32", "none", "plat"), + tags.Tag("py31", "none", "plat"), + tags.Tag("py30", "none", "plat"), + tags.Tag("cp3_11", "none", "any"), + tags.Tag("py3_11", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py3_10", "none", "any"), + tags.Tag("py39", "none", "any"), + tags.Tag("py38", "none", "any"), + tags.Tag("py37", "none", "any"), + tags.Tag("py36", "none", "any"), + tags.Tag("py35", "none", "any"), + tags.Tag("py34", "none", "any"), + tags.Tag("py33", "none", "any"), + tags.Tag("py32", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + def test_default_interpreter(self): result = list(tags.compatible_tags((3, 1), platforms=["plat"])) assert result == [ @@ -966,14 +1099,10 @@ def test_mac_cpython(self, mock_interpreter_name, monkeypatch): result = list(tags.sys_tags()) assert len(abis) == 1 assert result[0] == tags.Tag( - "cp{major}{minor}".format( - major=sys.version_info[0], minor=sys.version_info[1] - ), - abis[0], - platforms[0], + "cp" + tags._version_nodot(sys.version_info[:2]), abis[0], platforms[0] ) assert result[-1] == tags.Tag( - "py{}0".format(sys.version_info[0]), "none", "any" + "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any" ) def test_windows_cpython(self, mock_interpreter_name, monkeypatch): @@ -985,13 +1114,13 @@ def test_windows_cpython(self, mock_interpreter_name, monkeypatch): abis = list(tags._cpython_abis(sys.version_info[:2])) platforms = list(tags._generic_platforms()) result = list(tags.sys_tags()) - interpreter = "cp{major}{minor}".format( - major=sys.version_info[0], minor=sys.version_info[1] - ) + interpreter = "cp" + tags._version_nodot(sys.version_info[:2]) assert len(abis) == 1 expected = tags.Tag(interpreter, abis[0], platforms[0]) assert result[0] == expected - expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + expected = tags.Tag( + "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any" + ) assert result[-1] == expected def test_linux_cpython(self, mock_interpreter_name, monkeypatch): @@ -1003,12 +1132,12 @@ def test_linux_cpython(self, mock_interpreter_name, monkeypatch): abis = list(tags._cpython_abis(sys.version_info[:2])) platforms = list(tags._linux_platforms()) result = list(tags.sys_tags()) - expected_interpreter = "cp{major}{minor}".format( - major=sys.version_info[0], minor=sys.version_info[1] - ) + expected_interpreter = "cp" + tags._version_nodot(sys.version_info[:2]) assert len(abis) == 1 assert result[0] == tags.Tag(expected_interpreter, abis[0], platforms[0]) - expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + expected = tags.Tag( + "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any" + ) assert result[-1] == expected def test_generic(self, monkeypatch): @@ -1016,5 +1145,7 @@ def test_generic(self, monkeypatch): monkeypatch.setattr(tags, "interpreter_name", lambda: "generic") result = list(tags.sys_tags()) - expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + expected = tags.Tag( + "py" + tags._version_nodot((sys.version_info[0], 0)), "none", "any" + ) assert result[-1] == expected From b84b7362001d621f95d7bc2cfa3335f261398fcd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Mar 2020 13:03:31 +0530 Subject: [PATCH 095/114] Automated releases - Round 2 (#274) * Improve reporting of mismatching files in dist/ * Automatically add a message to the release tag * Automate release-time changelog updates * Apply suggestions from code review Co-Authored-By: Brett Cannon <54418+brettcannon@users.noreply.github.com> --- docs/development/release-process.rst | 7 +- noxfile.py | 95 ++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/docs/development/release-process.rst b/docs/development/release-process.rst index 6bf11b22c..f634ac40b 100644 --- a/docs/development/release-process.rst +++ b/docs/development/release-process.rst @@ -6,19 +6,14 @@ Release Process $ pip install nox -#. Modify the ``CHANGELOG.rst`` to include changes made since the last release - and update the section header for the new release. - #. Run the release automation with the required version number (YY.N):: $ nox -s release -- YY.N -#. Modify the ``CHANGELOG.rst`` to reflect the development version does not - have any changes since the last release. - #. Notify the other project owners of the release. .. note:: + Access needed for making the release are: - PyPI maintainer (or owner) access to `packaging` diff --git a/noxfile.py b/noxfile.py index f7a512bb6..bf874a761 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,6 +6,11 @@ import sys import glob import shutil +import difflib +import tempfile +import textwrap +import datetime +import contextlib import subprocess from pathlib import Path @@ -73,6 +78,7 @@ def docs(session): def release(session): package_name = "packaging" version_file = Path(f"{package_name}/__about__.py") + changelog_file = Path(f"CHANGELOG.rst") try: release_version = _get_version_from_arguments(session.posargs) @@ -83,13 +89,22 @@ def release(session): _check_working_directory_state(session) _check_git_state(session, release_version) - # Update to the release version. + # Prepare for release. + _changelog_update_unreleased_title(release_version, file=changelog_file) _bump(session, version=release_version, file=version_file, kind="release") # Tag the release commit. - session.run("git", "tag", "-s", release_version, external=True) + # fmt: off + session.run( + "git", "tag", + "-s", release_version, + "-m", f"Release {release_version}", + external=True, + ) + # fmt: on - # Bump the version for development. + # Prepare for development. + _changelog_add_unreleased_title(file=changelog_file) major, minor = map(int, release_version.split(".")) next_version = f"{major}.{minor + 1}.dev0" _bump(session, version=next_version, file=version_file, kind="development") @@ -101,11 +116,17 @@ def release(session): session.run("python", "setup.py", "sdist", "bdist_wheel") # Check what files are in dist/ for upload. - files = glob.glob(f"dist/*") - assert sorted(files) == [ - f"dist/{package_name}-{release_version}.tag.gz", + files = sorted(glob.glob(f"dist/*")) + expected = [ f"dist/{package_name}-{release_version}-py2.py3-none-any.whl", - ], f"Got the wrong files: {files}" + f"dist/{package_name}-{release_version}.tar.gz", + ] + if files != expected: + diff_generator = difflib.context_diff( + expected, files, fromfile="expected", tofile="got", lineterm="" + ) + diff = "\n".join(diff_generator) + session.error(f"Got the wrong files:\n{diff}") # Get back out into master. session.run("git", "checkout", "-q", "master", external=True) @@ -215,3 +236,63 @@ def _bump(session, *, version, file, kind): session.log(f"git commit") subprocess.run(["git", "add", str(file)]) subprocess.run(["git", "commit", "-m", f"Bump for {kind}"]) + + +@contextlib.contextmanager +def _replace_file(original_path): + # Create a temporary file. + fh, replacement_path = tempfile.mkstemp() + + try: + with os.fdopen(fh, "w") as replacement: + with open(original_path) as original: + yield original, replacement + except Exception: + raise + else: + shutil.copymode(original_path, replacement_path) + os.remove(original_path) + shutil.move(replacement_path, original_path) + + +def _changelog_update_unreleased_title(version, *, file): + """Update an "*unreleased*" heading to "{version} - {date}" + """ + yyyy_mm_dd = datetime.datetime.today().strftime("%Y-%m-%d") + title = f"{version} - {yyyy_mm_dd}" + + with _replace_file(file) as (original, replacement): + for line in original: + if line == "*unreleased*\n": + replacement.write(f"{title}\n") + replacement.write(len(title) * "~" + "\n") + # Skip processing the next line (the heading underline for *unreleased*) + # since we already wrote the heading underline. + next(original) + else: + replacement.write(line) + + +def _changelog_add_unreleased_title(*, file): + with _replace_file(file) as (original, replacement): + # Duplicate first 3 lines from the original file. + for _ in range(3): + line = next(original) + replacement.write(line) + + # Write the heading. + replacement.write( + textwrap.dedent( + """\ + *unreleased* + ~~~~~~~~~~~~ + + No unreleased changes. + + """ + ) + ) + + # Duplicate all the remaining lines. + for line in original: + replacement.write(line) From e449728a597a709a2e1eb7e8aac46a6500e741a5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Mar 2020 13:06:05 +0530 Subject: [PATCH 096/114] Bump for release --- packaging/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/__about__.py b/packaging/__about__.py index 08d2c892b..415a21a9b 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.1" +__version__ = "20.2" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From 060fa9550796503bd5349af3b9687da2a5c0f846 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Mar 2020 13:06:14 +0530 Subject: [PATCH 097/114] Bump for development --- packaging/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/__about__.py b/packaging/__about__.py index 415a21a9b..509930fec 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.2" +__version__ = "20.3.dev0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From 06ab8eee469c6143a8601a3fef5c4c168d0d7216 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Mar 2020 13:08:35 +0530 Subject: [PATCH 098/114] Bump for release --- packaging/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/__about__.py b/packaging/__about__.py index 509930fec..415a21a9b 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.3.dev0" +__version__ = "20.2" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From 4a340c716c989bf04a895afe91245858e5a56e90 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Mar 2020 13:08:35 +0530 Subject: [PATCH 099/114] Bump for development --- packaging/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/__about__.py b/packaging/__about__.py index 415a21a9b..509930fec 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.2" +__version__ = "20.3.dev0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From 274ed498570dc787b7f1d30b5b3d2f0dd7b77687 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Mar 2020 13:13:18 +0530 Subject: [PATCH 100/114] Actually stage changelog changes before release --- noxfile.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/noxfile.py b/noxfile.py index bf874a761..12c25b9be 100644 --- a/noxfile.py +++ b/noxfile.py @@ -91,6 +91,7 @@ def release(session): # Prepare for release. _changelog_update_unreleased_title(release_version, file=changelog_file) + session.run("git", "add", str(changelog_file), external=True) _bump(session, version=release_version, file=version_file, kind="release") # Tag the release commit. @@ -105,6 +106,8 @@ def release(session): # Prepare for development. _changelog_add_unreleased_title(file=changelog_file) + session.run("git", "add", str(changelog_file), external=True) + major, minor = map(int, release_version.split(".")) next_version = f"{major}.{minor + 1}.dev0" _bump(session, version=next_version, file=version_file, kind="development") @@ -112,6 +115,8 @@ def release(session): # Checkout the git tag. session.run("git", "checkout", "-q", release_version, external=True) + session.install("twine", "setuptools", "wheel") + # Build the distribution. session.run("python", "setup.py", "sdist", "bdist_wheel") From 1bb9e9122b741b43b45de8944f43fa6441310588 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Mar 2020 13:14:26 +0530 Subject: [PATCH 101/114] Update changelog to include 20.2 section --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a56d64638..98c976929 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,11 @@ Changelog *unreleased* ~~~~~~~~~~~~ +* Fix changelog for 20.2. + +20.2 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + * Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8, aarch64), to report the wrong bitness. From d58a8f1c91af7249ad86e9a1880dbaf6ea57dbde Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Mar 2020 13:18:02 +0530 Subject: [PATCH 102/114] Bump for release --- CHANGELOG.rst | 4 ++-- packaging/__about__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 98c976929..1d8ae108f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,8 @@ Changelog --------- -*unreleased* -~~~~~~~~~~~~ +20.3 - 2020-03-05 +~~~~~~~~~~~~~~~~~ * Fix changelog for 20.2. diff --git a/packaging/__about__.py b/packaging/__about__.py index 509930fec..5161d141b 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.3.dev0" +__version__ = "20.3" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From eb22c66fdbc5406594416375e10b2da8ca36f637 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Mar 2020 13:18:02 +0530 Subject: [PATCH 103/114] Bump for development --- CHANGELOG.rst | 5 +++++ packaging/__about__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d8ae108f..a0bc10b26 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog --------- +*unreleased* +~~~~~~~~~~~~ + +No unreleased changes. + 20.3 - 2020-03-05 ~~~~~~~~~~~~~~~~~ diff --git a/packaging/__about__.py b/packaging/__about__.py index 5161d141b..93421f01c 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.3" +__version__ = "20.4.dev0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" From d3f305de93230c44c2923f46b5a18f9a83607509 Mon Sep 17 00:00:00 2001 From: Derek Keeler Date: Sun, 22 Mar 2020 18:21:01 -0700 Subject: [PATCH 104/114] Add docstrings to packaging.tags.Tag and packaging.tags.parse_tag() (#281) Closes #278 Co-authored-by: Derek Keeler --- .gitignore | 1 + packaging/tags.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e0f870219..05e554a64 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .coverage .idea .venv* +.vscode/ .mypy_cache/ .pytest_cache/ diff --git a/packaging/tags.py b/packaging/tags.py index 300faab84..25d8e0080 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -58,6 +58,12 @@ 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"] @@ -108,6 +114,12 @@ def __repr__(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("."): @@ -541,7 +553,7 @@ def __init__(self, file): def unpack(fmt): # type: (str) -> int try: - result, = struct.unpack( + (result,) = struct.unpack( fmt, file.read(struct.calcsize(fmt)) ) # type: (int, ) except struct.error: From 20a9a33663d1a1fb780be74a6c817a43ede0fc19 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 26 Mar 2020 13:52:40 -0500 Subject: [PATCH 105/114] Canonicalize version before comparing specifiers (#283) * Add failing tests * Canonicalize version before comparing specifiers * Update changelog --- CHANGELOG.rst | 2 +- packaging/specifiers.py | 6 ++++-- tests/test_specifiers.py | 12 ++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a0bc10b26..2b6af82a3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Changelog *unreleased* ~~~~~~~~~~~~ -No unreleased changes. +* Canonicalize version before comparing specifiers. (:issue:`282`) 20.3 - 2020-03-05 ~~~~~~~~~~~~~~~~~ diff --git a/packaging/specifiers.py b/packaging/specifiers.py index 94987486d..fc07859ce 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -10,6 +10,7 @@ from ._compat import string_types, with_metaclass from ._typing import MYPY_CHECK_RUNNING +from .utils import canonicalize_version from .version import Version, LegacyVersion, parse if MYPY_CHECK_RUNNING: # pragma: no cover @@ -134,7 +135,8 @@ def __str__(self): def __hash__(self): # type: () -> int - return hash(self._spec) + operator, version = self._spec + return hash((operator, canonicalize_version(version))) def __eq__(self, other): # type: (object) -> bool @@ -146,7 +148,7 @@ def __eq__(self, other): elif not isinstance(other, self.__class__): return NotImplemented - return self._spec == other._spec + return hash(self) == hash(other) def __ne__(self, other): # type: (object) -> bool diff --git a/tests/test_specifiers.py b/tests/test_specifiers.py index a3feecc23..ee54dc84b 100644 --- a/tests/test_specifiers.py +++ b/tests/test_specifiers.py @@ -253,6 +253,12 @@ def test_comparison_true(self, left, right, op): assert op(left, Specifier(right)) assert op(Specifier(left), right) + @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")]) + def test_comparison_canonicalizes(self, left, right): + assert Specifier(left) == Specifier(right) + assert left == Specifier(right) + assert Specifier(left) == right + @pytest.mark.parametrize( ("left", "right", "op"), itertools.chain( @@ -961,6 +967,12 @@ def test_comparison_false(self, left, right, op): assert not op(left, SpecifierSet(right)) assert not op(SpecifierSet(left), right) + @pytest.mark.parametrize(("left", "right"), [("==2.8.0", "==2.8")]) + def test_comparison_canonicalizes(self, left, right): + assert SpecifierSet(left) == SpecifierSet(right) + assert left == SpecifierSet(right) + assert SpecifierSet(left) == right + def test_comparison_non_specifier(self): assert SpecifierSet("==1.0") != 12 assert not SpecifierSet("==1.0") == 12 From ed0a58f0dacf08c871ce7b687558d3891de75ca9 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez Date: Fri, 27 Mar 2020 17:48:08 +0100 Subject: [PATCH 106/114] specifiers: don't rely on hashes for equality (#284) --- packaging/specifiers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index fc07859ce..4eeef14f8 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -133,10 +133,14 @@ 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): # type: () -> int - operator, version = self._spec - return hash((operator, canonicalize_version(version))) + return hash(self._canonical_spec) def __eq__(self, other): # type: (object) -> bool @@ -148,7 +152,7 @@ def __eq__(self, other): elif not isinstance(other, self.__class__): return NotImplemented - return hash(self) == hash(other) + return self._canonical_spec == other._canonical_spec def __ne__(self, other): # type: (object) -> bool From a9f31ee35b6d79c815fe374b9376446c58a95f95 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 2 Apr 2020 12:03:41 -0500 Subject: [PATCH 107/114] Use SPDX identifiers for license metadata (#289) --- packaging/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/__about__.py b/packaging/__about__.py index 93421f01c..f842e43dd 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -23,5 +23,5 @@ __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__ From 9731c00837b8ad502754c856a0f251b0e0b19e4e Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Fri, 3 Apr 2020 01:37:27 +0530 Subject: [PATCH 108/114] Document major, minor and macro attributes of Version (#285) --- docs/version.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/version.rst b/docs/version.rst index 8eed7357d..803284d9b 100644 --- a/docs/version.rst +++ b/docs/version.rst @@ -86,6 +86,18 @@ Reference version number, including trailing zeroes but not including the epoch or any prerelease/development/postrelease suffixes + .. attribute:: major + + An integer representing the first item of :attr:`release` or ``0`` if unavailable. + + .. attribute:: minor + + An integer representing the second item of :attr:`release` or ``0`` if unavailable. + + .. attribute:: micro + + An integer representing the third item of :attr:`release` or ``0`` if unavailable. + .. attribute:: local A string representing the local version portion of this ``Version()`` From 61672bf9f507f38e84ce2786a1c42f55fa0a3153 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Wed, 8 Apr 2020 18:24:09 +0530 Subject: [PATCH 109/114] Add a NewType for normalized names (#292) * Make our mypy type-casting magic more robust Newer versions of mypy are better at following code flow, and assign Any types to our reimplemented cast function. This commit significantly restructures our typing.cast re-definition, to make it more robust. * Rename MYPY_CHECK_RUNNING -> TYPE_CHECKING This allows for some significant cleanups in our typing helpers, and makes it much easier to document our... workarounds. * Bump to newer mypy * Improve `canonicalize_name` safety with type hints --- .pre-commit-config.yaml | 2 +- CHANGELOG.rst | 4 ++++ packaging/_compat.py | 4 ++-- packaging/_typing.py | 29 +++++++++++++++++++---------- packaging/markers.py | 4 ++-- packaging/requirements.py | 4 ++-- packaging/specifiers.py | 4 ++-- packaging/tags.py | 4 ++-- packaging/utils.py | 13 ++++++++----- packaging/version.py | 4 ++-- 10 files changed, 44 insertions(+), 28 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 691246784..50d1f6c80 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.750 + rev: v0.770 hooks: - id: mypy exclude: '^(docs|tasks|tests)|setup\.py' diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2b6af82a3..3ce556a37 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,10 @@ Changelog ~~~~~~~~~~~~ * Canonicalize version before comparing specifiers. (:issue:`282`) +* Change type hint for ``canonicalize_name`` to return + ``packaging.utils.NormalizedName``. + This enables the use of static typing tools (like mypy) to detect mixing of + normalized and un-normalized names. 20.3 - 2020-03-05 ~~~~~~~~~~~~~~~~~ diff --git a/packaging/_compat.py b/packaging/_compat.py index a145f7eeb..e54bd4ede 100644 --- a/packaging/_compat.py +++ b/packaging/_compat.py @@ -5,9 +5,9 @@ import sys -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import Any, Dict, Tuple, Type diff --git a/packaging/_typing.py b/packaging/_typing.py index dc6dfce7a..77a8b9185 100644 --- a/packaging/_typing.py +++ b/packaging/_typing.py @@ -18,22 +18,31 @@ In packaging, all static-typing related imports should be guarded as follows: - from packaging._typing import MYPY_CHECK_RUNNING + from packaging._typing import TYPE_CHECKING - if MYPY_CHECK_RUNNING: + if TYPE_CHECKING: from typing import ... Ref: https://github.com/python/mypy/issues/3216 """ -MYPY_CHECK_RUNNING = False +__all__ = ["TYPE_CHECKING", "cast"] -if MYPY_CHECK_RUNNING: # pragma: no cover - import typing - - cast = typing.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: - # typing's cast() is needed at runtime, but we don't want to import typing. - # Thus, we use a dummy no-op version, which we tell mypy to ignore. - def cast(type_, value): # type: ignore + # executed at runtime + def cast(type_, value): # noqa return value diff --git a/packaging/markers.py b/packaging/markers.py index f01747113..87cd3f958 100644 --- a/packaging/markers.py +++ b/packaging/markers.py @@ -13,10 +13,10 @@ from pyparsing import Literal as L # noqa from ._compat import string_types -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING from .specifiers import Specifier, InvalidSpecifier -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import Any, Callable, Dict, List, Optional, Tuple, Union Operator = Callable[[str, str], bool] diff --git a/packaging/requirements.py b/packaging/requirements.py index 1b547927d..91f81ede0 100644 --- a/packaging/requirements.py +++ b/packaging/requirements.py @@ -11,11 +11,11 @@ from pyparsing import Literal as L # noqa from six.moves.urllib import parse as urlparse -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING from .markers import MARKER_EXPR, Marker from .specifiers import LegacySpecifier, Specifier, SpecifierSet -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import List diff --git a/packaging/specifiers.py b/packaging/specifiers.py index 4eeef14f8..9f7dd2787 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -9,11 +9,11 @@ import re from ._compat import string_types, with_metaclass -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING from .utils import canonicalize_version from .version import Version, LegacyVersion, parse -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import ( List, Dict, diff --git a/packaging/tags.py b/packaging/tags.py index 25d8e0080..9064910b8 100644 --- a/packaging/tags.py +++ b/packaging/tags.py @@ -22,9 +22,9 @@ import sysconfig import warnings -from ._typing import MYPY_CHECK_RUNNING, cast +from ._typing import TYPE_CHECKING, cast -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import ( Dict, FrozenSet, diff --git a/packaging/utils.py b/packaging/utils.py index 44f1bf987..19579c1a0 100644 --- a/packaging/utils.py +++ b/packaging/utils.py @@ -5,19 +5,22 @@ import re -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING, cast from .version import InvalidVersion, Version -if MYPY_CHECK_RUNNING: # pragma: no cover - from typing import Union +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) -> str + # 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): diff --git a/packaging/version.py b/packaging/version.py index f39a2a12a..00371e86a 100644 --- a/packaging/version.py +++ b/packaging/version.py @@ -8,9 +8,9 @@ import re from ._structures import Infinity, NegativeInfinity -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union from ._structures import InfinityType, NegativeInfinityType From 221443a8a9aa3bdc89726c01cf34e57159ba6ae7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh Date: Tue, 28 Apr 2020 00:24:49 +0530 Subject: [PATCH 110/114] Document version classes comparison behaviour (#297) --- docs/version.rst | 50 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/version.rst b/docs/version.rst index 803284d9b..ff58c51d7 100644 --- a/docs/version.rst +++ b/docs/version.rst @@ -151,6 +151,52 @@ Reference :param str version: The string representation of a version which will be used as is. + .. note:: + + :class:`LegacyVersion` instances are always ordered lower than :class:`Version` instances. + + >>> from packaging.version import Version, LegacyVersion + >>> v1 = Version("1.0") + >>> v2 = LegacyVersion("1.0") + >>> v1 > v2 + True + >>> v3 = LegacyVersion("1.3") + >>> v1 > v3 + True + + Also note that some strings are still valid PEP 440 strings (:class:`Version`), even if they look very similar to + other versions that are not (:class:`LegacyVersion`). Examples include versions with `Pre-release spelling`_ and + `Post-release spelling`_. + + >>> from packaging.version import parse + >>> v1 = parse('0.9.8a') + >>> v2 = parse('0.9.8beta') + >>> v3 = parse('0.9.8r') + >>> v4 = parse('0.9.8rev') + >>> v5 = parse('0.9.8t') + >>> v1 + + >>> v1.is_prerelease + True + >>> v2 + + >>> v2.is_prerelease + True + >>> v3 + + >>> v3.is_postrelease + True + >>> v4 + + >>> v4.is_postrelease + True + >>> v5 + + >>> v5.is_prerelease + False + >>> v5.is_postrelease + False + .. attribute:: public A string representing the public version portion of this @@ -237,4 +283,6 @@ Reference ``re.VERBOSE`` and ``re.IGNORECASE`` flags set. -.. _`PEP 440`: https://www.python.org/dev/peps/pep-0440/ +.. _PEP 440: https://www.python.org/dev/peps/pep-0440/ +.. _Pre-release spelling : https://www.python.org/dev/peps/pep-0440/#pre-release-spelling +.. _Post-release spelling : https://www.python.org/dev/peps/pep-0440/#post-release-spelling From d2e29ede9ee0e85b66ac550f62766a2a0b0f0fd2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 28 Apr 2020 22:59:45 +0530 Subject: [PATCH 111/114] Rename GitHub Actions tasks (#298) --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 532c11ea6..229fd51f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,11 +12,12 @@ on: jobs: test: + name: ${{ matrix.os }} / ${{ matrix.python_version }} runs-on: ${{ matrix.os }}-latest strategy: fail-fast: false matrix: - os: [ubuntu, windows, macOS] + os: [Ubuntu, Windows, macOS] # Python 3.4 is not available from actions/setup-python@v1. python_version: ['2.7', '3.5', '3.6', '3.7', '3.8', 'pypy2', 'pypy3'] exclude: From fbe51442f084d553e83c1a586f1b44e215657f3c Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Mon, 11 May 2020 20:16:02 -0500 Subject: [PATCH 112/114] Pass positional args to pytest invocation (#301) --- noxfile.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 12c25b9be..925215cfe 100644 --- a/noxfile.py +++ b/noxfile.py @@ -28,7 +28,15 @@ def coverage(*args): session.install("coverage<5.0.0", "pretend", "pytest", "pip>=9.0.2") if "pypy" not in session.python: - coverage("run", "--source", "packaging/", "-m", "pytest", "--strict") + coverage( + "run", + "--source", + "packaging/", + "-m", + "pytest", + "--strict", + *session.posargs, + ) coverage("report", "-m", "--fail-under", "100") else: # Don't do coverage tracking for PyPy, since it's SLOW. From db291c7bdac5c6684b6256562903b361baf518fa Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 13 May 2020 04:51:11 -0500 Subject: [PATCH 113/114] Fix LEQ/GEQ for SpecifierSets when considering versions with a local version label (#304) * Add failing test * Ignore local version when comparing PEP 440 specifies: "If the specified version identifier is a public version identifier (no local version label), then the local version label of any candidate versions MUST be ignored when matching versions." * Add comments --- packaging/specifiers.py | 12 ++++++++++-- tests/test_specifiers.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packaging/specifiers.py b/packaging/specifiers.py index 9f7dd2787..fe09bb1db 100644 --- a/packaging/specifiers.py +++ b/packaging/specifiers.py @@ -516,12 +516,20 @@ def _compare_not_equal(self, prospective, spec): @_require_version_compare def _compare_less_than_equal(self, prospective, spec): # type: (ParsedVersion, str) -> bool - return prospective <= Version(spec) + + # 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): # type: (ParsedVersion, str) -> bool - return prospective >= Version(spec) + + # 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_str): diff --git a/tests/test_specifiers.py b/tests/test_specifiers.py index ee54dc84b..6126f3657 100644 --- a/tests/test_specifiers.py +++ b/tests/test_specifiers.py @@ -976,3 +976,17 @@ def test_comparison_canonicalizes(self, left, right): def test_comparison_non_specifier(self): assert SpecifierSet("==1.0") != 12 assert not SpecifierSet("==1.0") == 12 + + @pytest.mark.parametrize( + ("version", "specifier", "expected"), + [ + ("1.0.0+local", "==1.0.0", True), + ("1.0.0+local", "!=1.0.0", False), + ("1.0.0+local", "<=1.0.0", True), + ("1.0.0+local", ">=1.0.0", True), + ("1.0.0+local", "<1.0.0", False), + ("1.0.0+local", ">1.0.0", False), + ], + ) + def test_comparison_ignores_local(self, version, specifier, expected): + assert (Version(version) in SpecifierSet(specifier)) == expected From ded06cedf6e20680eea0363fac894cb4a09e7831 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 19 May 2020 11:59:41 +0530 Subject: [PATCH 114/114] Bump for release --- CHANGELOG.rst | 4 ++-- packaging/__about__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3ce556a37..b38a7494d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,8 @@ Changelog --------- -*unreleased* -~~~~~~~~~~~~ +20.4 - 2020-05-19 +~~~~~~~~~~~~~~~~~ * Canonicalize version before comparing specifiers. (:issue:`282`) * Change type hint for ``canonicalize_name`` to return diff --git a/packaging/__about__.py b/packaging/__about__.py index f842e43dd..4d998578d 100644 --- a/packaging/__about__.py +++ b/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.4.dev0" +__version__ = "20.4" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io"