From cea9f32daef08a2172bc7b44cce9952db1cb7ca1 Mon Sep 17 00:00:00 2001 From: Daniel Katz Date: Fri, 28 Aug 2020 12:55:17 -0400 Subject: [PATCH] Support multiple `abi` and `platform` values for `pip download`. --- docs/html/reference/pip_download.rst | 29 ++++++++++++++++++ news/6121.feature | 1 + src/pip/_internal/cli/cmdoptions.py | 19 +++++++++--- src/pip/_internal/models/target_python.py | 30 +++++++++---------- src/pip/_internal/utils/compatibility_tags.py | 19 +++++------- tests/unit/test_models_wheel.py | 20 ++++++------- tests/unit/test_target_python.py | 8 ++--- tests/unit/test_utils_compatibility_tags.py | 4 +-- 8 files changed, 84 insertions(+), 46 deletions(-) create mode 100644 news/6121.feature diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index 80acc1942..b600d15e5 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -197,3 +197,32 @@ Examples C:\> dir pip-8.1.1-py2.py3-none-any.whl pip-8.1.1-py2.py3-none-any.whl + +#. Download a package supporting one of several ABIs and platforms. + This is useful when fetching wheels for a well-defined interpreter, whose + supported ABIs and platforms are known and fixed, different than the one pip is + running under. + + .. tab:: Unix/macOS + + .. code-block:: console + + $ python -m pip download \ + --only-binary=:all: \ + --platform manylinux1_x86_64 --platform linux_x86_64 --platform any \ + --python-version 36 \ + --implementation cp \ + --abi cp36m --abi cp36 --abi abi3 --abi none \ + SomePackage + + .. tab:: Windows + + .. code-block:: console + + C:> py -m pip download ^ + --only-binary=:all: ^ + --platform manylinux1_x86_64 --platform linux_x86_64 --platform any ^ + --python-version 36 ^ + --implementation cp ^ + --abi cp36m --abi cp36 --abi abi3 --abi none ^ + SomePackage diff --git a/news/6121.feature b/news/6121.feature new file mode 100644 index 000000000..016426e88 --- /dev/null +++ b/news/6121.feature @@ -0,0 +1 @@ +Allow comma-separated values for --abi and --platform. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index e96eac586..fe2fae356 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -31,7 +31,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: from optparse import OptionParser, Values - from typing import Any, Callable, Dict, Optional, Tuple + from typing import Any, Callable, Dict, List, Optional, Tuple from pip._internal.cli.parser import ConfigOptionParser @@ -588,7 +588,7 @@ abi = partial( metavar='abi', default=None, help=("Only use wheels compatible with Python " - "abi , e.g. 'pypy_41'. If not specified, then the " + "abi , e.g. 'pypy_41,none'. If not specified, then the " "current interpreter abi tag is used. Generally " "you will need to specify --implementation, " "--platform, and --python-version when using " @@ -606,10 +606,21 @@ def add_target_python_options(cmd_opts): def make_target_python(options): # type: (Values) -> TargetPython + + # abi can be a comma-separated list of values. + abis = options.abi # type: Optional[List[str]] + if options.abi: + abis = options.abi.split(',') + + # platform can also be a comma-separated list of values. + platforms = options.platform # type: Optional[List[str]] + if options.platform: + platforms = options.platform.split(',') + target_python = TargetPython( - platform=options.platform, + platforms=platforms, py_version_info=options.python_version, - abi=options.abi, + abis=abis, implementation=options.implementation, ) diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index ad7e506a6..4593dc854 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -19,9 +19,9 @@ class TargetPython(object): __slots__ = [ "_given_py_version_info", - "abi", + "abis", "implementation", - "platform", + "platforms", "py_version", "py_version_info", "_valid_tags", @@ -29,23 +29,23 @@ class TargetPython(object): def __init__( self, - platform=None, # type: Optional[str] + platforms=None, # type: Optional[List[str]] py_version_info=None, # type: Optional[Tuple[int, ...]] - abi=None, # type: Optional[str] + abis=None, # type: Optional[List[str]] implementation=None, # type: Optional[str] ): # type: (...) -> None """ - :param platform: A string or None. If None, searches for packages - that are supported by the current system. Otherwise, will find - packages that can be built on the platform passed in. These + :param platforms: A list of strings or None. If None, searches for + packages that are supported by the current system. Otherwise, will + find packages that can be built on the platforms passed in. These packages will only be downloaded for distribution: they will not be built locally. :param py_version_info: An optional tuple of ints representing the Python version information to use (e.g. `sys.version_info[:3]`). This can have length 1, 2, or 3 when provided. - :param abi: A string or None. This is passed to compatibility_tags.py's - get_supported() function as is. + :param abis: A list of strings or None. This is passed to + compatibility_tags.py's get_supported() function as is. :param implementation: A string or None. This is passed to compatibility_tags.py's get_supported() function as is. """ @@ -59,9 +59,9 @@ class TargetPython(object): py_version = '.'.join(map(str, py_version_info[:2])) - self.abi = abi + self.abis = abis self.implementation = implementation - self.platform = platform + self.platforms = platforms self.py_version = py_version self.py_version_info = py_version_info @@ -80,9 +80,9 @@ class TargetPython(object): ) key_values = [ - ('platform', self.platform), + ('platforms', self.platforms), ('version_info', display_version), - ('abi', self.abi), + ('abis', self.abis), ('implementation', self.implementation), ] return ' '.join( @@ -108,8 +108,8 @@ class TargetPython(object): tags = get_supported( version=version, - platform=self.platform, - abi=self.abi, + platforms=self.platforms, + abis=self.abis, impl=self.implementation, ) self._valid_tags = tags diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index 4f21874ec..eb1727e3d 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -105,9 +105,9 @@ def _get_custom_interpreter(implementation=None, version=None): def get_supported( version=None, # type: Optional[str] - platform=None, # type: Optional[str] + platforms=None, # type: Optional[List[str]] impl=None, # type: Optional[str] - abi=None # type: Optional[str] + abis=None # type: Optional[List[str]] ): # type: (...) -> List[Tag] """Return a list of supported tags for each version specified in @@ -115,11 +115,11 @@ def get_supported( :param version: a string version, of the form "33" or "32", or None. The version will be assumed to support our ABI. - :param platform: specify the exact platform you want valid + :param platform: specify a list of platforms you want valid tags for, or None. If None, use the local system platform. :param impl: specify the exact implementation you want valid tags for, or None. If None, use the local interpreter impl. - :param abi: specify the exact abi you want valid + :param abis: specify a list of abis you want valid tags for, or None. If None, use the local interpreter abi. """ supported = [] # type: List[Tag] @@ -130,13 +130,10 @@ def get_supported( interpreter = _get_custom_interpreter(impl, version) - abis = None # type: Optional[List[str]] - if abi is not None: - abis = [abi] - - platforms = None # type: Optional[List[str]] - if platform is not None: - platforms = _get_custom_platforms(platform) + if platforms and len(platforms) == 1: + # Only expand list of platforms if a single platform was provided. + # Otherwise, assume that the list provided is comprehensive. + platforms = _get_custom_platforms(platforms[0]) is_cpython = (impl or interpreter_name()) == "cp" if is_cpython: diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py index f1fef6f09..05ee74262 100644 --- a/tests/unit/test_models_wheel.py +++ b/tests/unit/test_models_wheel.py @@ -76,7 +76,7 @@ class TestWheelFile(object): Wheels built for macOS 10.6 are supported on 10.9 """ tags = compatibility_tags.get_supported( - '27', platform='macosx_10_9_intel', impl='cp' + '27', platforms=['macosx_10_9_intel'], impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl') assert w.supported(tags=tags) @@ -88,7 +88,7 @@ class TestWheelFile(object): Wheels built for macOS 10.9 are not supported on 10.6 """ tags = compatibility_tags.get_supported( - '27', platform='macosx_10_6_intel', impl='cp' + '27', platforms=['macosx_10_6_intel'], impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert not w.supported(tags=tags) @@ -98,22 +98,22 @@ class TestWheelFile(object): Multi-arch wheels (intel) are supported on components (i386, x86_64) """ universal = compatibility_tags.get_supported( - '27', platform='macosx_10_5_universal', impl='cp' + '27', platforms=['macosx_10_5_universal'], impl='cp' ) intel = compatibility_tags.get_supported( - '27', platform='macosx_10_5_intel', impl='cp' + '27', platforms=['macosx_10_5_intel'], impl='cp' ) x64 = compatibility_tags.get_supported( - '27', platform='macosx_10_5_x86_64', impl='cp' + '27', platforms=['macosx_10_5_x86_64'], impl='cp' ) i386 = compatibility_tags.get_supported( - '27', platform='macosx_10_5_i386', impl='cp' + '27', platforms=['macosx_10_5_i386'], impl='cp' ) ppc = compatibility_tags.get_supported( - '27', platform='macosx_10_5_ppc', impl='cp' + '27', platforms=['macosx_10_5_ppc'], impl='cp' ) ppc64 = compatibility_tags.get_supported( - '27', platform='macosx_10_5_ppc64', impl='cp' + '27', platforms=['macosx_10_5_ppc64'], impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_5_intel.whl') @@ -136,10 +136,10 @@ class TestWheelFile(object): Single-arch wheels (x86_64) are not supported on multi-arch (intel) """ universal = compatibility_tags.get_supported( - '27', platform='macosx_10_5_universal', impl='cp' + '27', platforms=['macosx_10_5_universal'], impl='cp' ) intel = compatibility_tags.get_supported( - '27', platform='macosx_10_5_intel', impl='cp' + '27', platforms=['macosx_10_5_intel'], impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_5_i386.whl') diff --git a/tests/unit/test_target_python.py b/tests/unit/test_target_python.py index 0dc2af22b..a314988eb 100644 --- a/tests/unit/test_target_python.py +++ b/tests/unit/test_target_python.py @@ -45,16 +45,16 @@ class TestTargetPython: ({}, ''), (dict(py_version_info=(3, 6)), "version_info='3.6'"), ( - dict(platform='darwin', py_version_info=(3, 6)), - "platform='darwin' version_info='3.6'", + dict(platforms=['darwin'], py_version_info=(3, 6)), + "platforms=['darwin'] version_info='3.6'", ), ( dict( - platform='darwin', py_version_info=(3, 6), abi='cp36m', + platforms=['darwin'], py_version_info=(3, 6), abis=['cp36m'], implementation='cp' ), ( - "platform='darwin' version_info='3.6' abi='cp36m' " + "platforms=['darwin'] version_info='3.6' abis=['cp36m'] " "implementation='cp'" ), ), diff --git a/tests/unit/test_utils_compatibility_tags.py b/tests/unit/test_utils_compatibility_tags.py index 12c8da453..64f59a2f9 100644 --- a/tests/unit/test_utils_compatibility_tags.py +++ b/tests/unit/test_utils_compatibility_tags.py @@ -63,7 +63,7 @@ class TestManylinux2010Tags(object): Specifying manylinux2010 implies manylinux1. """ groups = {} - supported = compatibility_tags.get_supported(platform=manylinux2010) + supported = compatibility_tags.get_supported(platforms=[manylinux2010]) for tag in supported: groups.setdefault( (tag.interpreter, tag.abi), [] @@ -87,7 +87,7 @@ class TestManylinux2014Tags(object): Specifying manylinux2014 implies manylinux2010/manylinux1. """ groups = {} - supported = compatibility_tags.get_supported(platform=manylinuxA) + supported = compatibility_tags.get_supported(platforms=[manylinuxA]) for tag in supported: groups.setdefault( (tag.interpreter, tag.abi), []