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.rst b/news/6121.feature.rst new file mode 100644 index 000000000..16b272a69 --- /dev/null +++ b/news/6121.feature.rst @@ -0,0 +1 @@ +Allow multiple values for --abi and --platform. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index e96eac586..86bc740f8 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -97,8 +97,8 @@ def check_dist_restriction(options, check_target=False): """ dist_restriction_set = any([ options.python_version, - options.platform, - options.abi, + options.platforms, + options.abis, options.implementation, ]) @@ -490,14 +490,16 @@ def only_binary(): ) -platform = partial( +platforms = partial( Option, '--platform', - dest='platform', + dest='platforms', metavar='platform', + action='append', default=None, - help=("Only use wheels compatible with . " - "Defaults to the platform of the running system."), + help=("Only use wheels compatible with . Defaults to the " + "platform of the running system. Use this option multiple times to " + "specify multiple platforms supported by the target interpreter."), ) # type: Callable[..., Option] @@ -581,35 +583,36 @@ implementation = partial( ) # type: Callable[..., Option] -abi = partial( +abis = partial( Option, '--abi', - dest='abi', + dest='abis', metavar='abi', + action='append', default=None, - help=("Only use wheels compatible with Python " - "abi , e.g. 'pypy_41'. If not specified, then the " - "current interpreter abi tag is used. Generally " - "you will need to specify --implementation, " - "--platform, and --python-version when using " - "this option."), + help=("Only use wheels compatible with Python abi , e.g. 'pypy_41'. " + "If not specified, then the current interpreter abi tag is used. " + "Use this option multiple times to specify multiple abis supported " + "by the target interpreter. Generally you will need to specify " + "--implementation, --platform, and --python-version when using this " + "option."), ) # type: Callable[..., Option] def add_target_python_options(cmd_opts): # type: (OptionGroup) -> None - cmd_opts.add_option(platform()) + cmd_opts.add_option(platforms()) cmd_opts.add_option(python_version()) cmd_opts.add_option(implementation()) - cmd_opts.add_option(abi()) + cmd_opts.add_option(abis()) def make_target_python(options): # type: (Values) -> TargetPython target_python = TargetPython( - platform=options.platform, + platforms=options.platforms, py_version_info=options.python_version, - abi=options.abi, + abis=options.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..6780f9d9d 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -86,6 +86,24 @@ def _get_custom_platforms(arch): return arches +def _expand_allowed_platforms(platforms): + # type: (Optional[List[str]]) -> Optional[List[str]] + if not platforms: + return None + + seen = set() + result = [] + + for p in platforms: + if p in seen: + continue + additions = [c for c in _get_custom_platforms(p) if c not in seen] + seen.update(additions) + result.extend(additions) + + return result + + def _get_python_version(version): # type: (str) -> PythonVersion if len(version) > 1: @@ -105,9 +123,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 +133,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 +148,7 @@ 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) + platforms = _expand_allowed_platforms(platforms) is_cpython = (impl or interpreter_name()) == "cp" if is_cpython: diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 3291d580d..2eee51b08 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -309,6 +309,21 @@ def test_download_specify_platform(script, data): Path('scratch') / 'fake-2.0-py2.py3-none-linux_x86_64.whl' ) + # Test with multiple supported platforms specified. + data.reset() + fake_wheel(data, 'fake-3.0-py2.py3-none-linux_x86_64.whl') + result = script.pip( + 'download', '--no-index', '--find-links', data.find_links, + '--only-binary=:all:', + '--dest', '.', + '--platform', 'manylinux1_x86_64', '--platform', 'linux_x86_64', + '--platform', 'any', + 'fake==3' + ) + result.did_create( + Path('scratch') / 'fake-3.0-py2.py3-none-linux_x86_64.whl' + ) + class TestDownloadPlatformManylinuxes(object): """ @@ -575,6 +590,22 @@ def test_download_specify_abi(script, data): expect_error=True, ) + data.reset() + fake_wheel(data, 'fake-1.0-fk2-otherabi-fake_platform.whl') + result = script.pip( + 'download', '--no-index', '--find-links', data.find_links, + '--only-binary=:all:', + '--dest', '.', + '--python-version', '2', + '--implementation', 'fk', + '--platform', 'fake_platform', + '--abi', 'fakeabi', '--abi', 'otherabi', '--abi', 'none', + 'fake' + ) + result.did_create( + Path('scratch') / 'fake-1.0-fk2-otherabi-fake_platform.whl' + ) + def test_download_specify_implementation(script, data): """ 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), []