Skip to content

Support multiple abi and platform values for pip download. #8820

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/html/reference/pip_download.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions news/6121.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow multiple values for --abi and --platform.
39 changes: 21 additions & 18 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
])

Expand Down Expand Up @@ -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 <platform>. "
"Defaults to the platform of the running system."),
help=("Only use wheels compatible with <platform>. 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]


Expand Down Expand Up @@ -581,35 +583,36 @@ def _handle_python_version(option, opt_str, value, parser):
) # 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 <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 <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,
)

Expand Down
30 changes: 15 additions & 15 deletions src/pip/_internal/models/target_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,33 @@ class TargetPython(object):

__slots__ = [
"_given_py_version_info",
"abi",
"abis",
"implementation",
"platform",
"platforms",
"py_version",
"py_version_info",
"_valid_tags",
]

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.
"""
Expand All @@ -59,9 +59,9 @@ def __init__(

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

Expand All @@ -80,9 +80,9 @@ def format_given(self):
)

key_values = [
('platform', self.platform),
('platforms', self.platforms),
('version_info', display_version),
('abi', self.abi),
('abis', self.abis),
('implementation', self.implementation),
]
return ' '.join(
Expand All @@ -108,8 +108,8 @@ def get_tags(self):

tags = get_supported(
version=version,
platform=self.platform,
abi=self.abi,
platforms=self.platforms,
abis=self.abis,
impl=self.implementation,
)
self._valid_tags = tags
Expand Down
34 changes: 23 additions & 11 deletions src/pip/_internal/utils/compatibility_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -105,21 +123,21 @@ 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
`versions`.

: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]
Expand All @@ -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:
Expand Down
31 changes: 31 additions & 0 deletions tests/functional/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down
20 changes: 10 additions & 10 deletions tests/unit/test_models_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_supported_osx_version(self):
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)
Expand All @@ -88,7 +88,7 @@ def test_not_supported_osx_version(self):
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)
Expand All @@ -98,22 +98,22 @@ def test_supported_multiarch_darwin(self):
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')
Expand All @@ -136,10 +136,10 @@ def test_not_supported_multiarch_darwin(self):
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')
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/test_target_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ def test_init__py_version_info_none(self):
({}, ''),
(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'"
),
),
Expand Down
Loading