Skip to content

Add --platform and --supported-interpreter-version options to the pip install command #2965

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

Closed
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
2 changes: 1 addition & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ pytest-capturelog
pytest-cov
pytest-timeout
pytest-xdist
mock
mock<1.1
scripttest>=1.3
https://github.com/pypa/virtualenv/archive/develop.zip#egg=virtualenv
68 changes: 65 additions & 3 deletions pip/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pip.req import RequirementSet
from pip.basecommand import RequirementCommand
from pip.locations import virtualenv_no_global, distutils_scheme
from pip.pep425tags import get_platform
from pip.index import PackageFinder
from pip.exceptions import (
InstallationError, CommandError, PreviousBuildDirError,
Expand Down Expand Up @@ -82,6 +83,33 @@ def __init__(self, *args, **kw):
"regardless of what's already installed."),
)

cmd_opts.add_option(
'--platform',
dest='platform',
metavar='platform',
default=get_platform(),
help=("Specifically download wheels compatible with <platform> "
"where the default is the platfrom of the local computer. "
"This option may only be used if --download is also being "
"used."),
)

cmd_opts.add_option(
'--interpreter-version',
dest='interpreter_version',
metavar='version',
default='',
help=("Specifically download wheels compatible with Python "
"interpreter version <version>. If not specified, then the "
"current system interpreter version is used. This option "
"may only be used if --download is also being used. This "
"is a stricter approach compared to the native search "
"performed without this option specified: this does not "
"accept previous minor versions of Python. It is meant to "
"be used when you want to download packages that support an "
"exact version of Python."),
)

cmd_opts.add_option(cmdoptions.download_cache())
cmd_opts.add_option(cmdoptions.src())

Expand Down Expand Up @@ -175,7 +203,14 @@ def __init__(self, *args, **kw):
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, cmd_opts)

def _build_package_finder(self, options, index_urls, session):
def _build_package_finder(
self,
options,
index_urls,
session,
platform,
desired_interp_versions
):
"""
Create a package finder appropriate to this install command.
This method is meant to be overridden by subclasses, not
Expand All @@ -192,6 +227,8 @@ def _build_package_finder(self, options, index_urls, session):
allow_all_prereleases=options.pre,
process_dependency_links=options.process_dependency_links,
session=session,
platform=platform,
versions=desired_interp_versions,
)

def run(self, options, args):
Expand All @@ -201,6 +238,26 @@ def run(self, options, args):
if options.download_dir:
options.ignore_installed = True

desired_platform = options.platform
if desired_platform != get_platform() and not options.download_dir:
raise CommandError(
"Usage: Cannot use --platform without also using "
"--download option."
)

if options.interpreter_version:
if options.download_dir:
desired_interp_versions = [
options.interpreter_version
]
else:
raise CommandError(
"Usage: Cannot use --interpreter-version "
"without also using --download option."
)
else:
desired_interp_versions = None

if options.build_dir:
options.build_dir = os.path.abspath(options.build_dir)

Expand Down Expand Up @@ -243,8 +300,13 @@ def run(self, options, args):
)

with self._build_session(options) as session:

finder = self._build_package_finder(options, index_urls, session)
finder = self._build_package_finder(
options,
index_urls,
session,
desired_platform,
desired_interp_versions
)
build_delete = (not (options.no_clean or options.build_dir))
wheel_cache = WheelCache(options.cache_dir, options.format_control)
if options.cache_dir and not check_path_owner(options.cache_dir):
Expand Down
40 changes: 31 additions & 9 deletions pip/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from pip.download import HAS_TLS, url_to_path, path_to_url
from pip.models import PyPI
from pip.wheel import Wheel, wheel_ext
from pip.pep425tags import supported_tags, supported_tags_noarch, get_platform
from pip.pep425tags import get_supported, get_platform
from pip._vendor import html5lib, requests, pkg_resources, six
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.requests.exceptions import SSLError
Expand Down Expand Up @@ -103,12 +103,20 @@ def __init__(self, find_links, index_urls,
allow_external=(), allow_unverified=(),
allow_all_external=False, allow_all_prereleases=False,
trusted_hosts=None, process_dependency_links=False,
session=None, format_control=None):
session=None, format_control=None, platform=None,
versions=None):
"""Create a PackageFinder.

:param format_control: A FormatControl object or None. Used to control
the selection of source packages / binary packages when consulting
the index and links.
: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. It is
understood that these packages will only be downloaded for
distribution: they will not be built locally.
:param versions: A list of strings or None. This is passed directly
to pep425tags.py in the get_supported() method.
"""
if session is None:
raise TypeError(
Expand Down Expand Up @@ -174,6 +182,19 @@ def __init__(self, find_links, index_urls,
# The Session we'll use to make requests
self.session = session

# The platform for which to find compatible packages
self.platform = platform or get_platform()

# The valid tags to check potential found wheel candidates against
self.valid_tags = get_supported(
versions=versions,
specificplatform=self.platform
)
self.valid_tags_noarch = get_supported(
versions=versions,
noarch=True
)

# If we don't have TLS enabled, then WARN if anyplace we're looking
# relies on TLS.
if not HAS_TLS:
Expand Down Expand Up @@ -248,24 +269,24 @@ def _candidate_sort_key(self, candidate):
If not finding wheels, then sorted by version only.
If finding wheels, then the sort order is by version, then:
1. existing installs
2. wheels ordered via Wheel.support_index_min()
2. wheels ordered via Wheel.support_index_min(self.valid_tags)
3. source archives
Note: it was considered to embed this logic into the Link
comparison operators, but then different sdist links
with the same version, would have to be considered equal
"""
support_num = len(supported_tags)
support_num = len(self.valid_tags)
if candidate.location == INSTALLED_VERSION:
pri = 1
elif candidate.location.is_wheel:
# can raise InvalidWheelFilename
wheel = Wheel(candidate.location.filename)
if not wheel.supported():
if not wheel.supported(self.valid_tags):
raise UnsupportedWheel(
"%s is not a supported wheel for this platform. It "
"can't be sorted." % wheel.filename
)
pri = -(wheel.support_index_min())
pri = -(wheel.support_index_min(self.valid_tags))
else: # sdist
pri = -(support_num)
return (candidate.version, pri)
Expand Down Expand Up @@ -703,7 +724,7 @@ def _log_skipped_link(self, link, reason):

def _link_package_versions(self, link, search):
"""Return an InstallationCandidate or None"""
platform = get_platform()
platform = self.platform

version = None
if link.egg_fragment:
Expand Down Expand Up @@ -736,7 +757,8 @@ def _link_package_versions(self, link, search):
self._log_skipped_link(
link, 'wrong project name (not %s)' % search.supplied)
return
if not wheel.supported():

if not wheel.supported(self.valid_tags):
self._log_skipped_link(
link, 'it is not compatible with this Python')
return
Expand All @@ -757,7 +779,7 @@ def _link_package_versions(self, link, search):
urllib_parse.urlparse(
comes_from.url
).netloc.endswith(PyPI.netloc)):
if not wheel.supported(tags=supported_tags_noarch):
if not wheel.supported(tags=self.valid_tags_noarch):
self._log_skipped_link(
link,
"it is a pypi-hosted binary "
Expand Down
6 changes: 4 additions & 2 deletions pip/pep425tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ def get_platform():
return distutils.util.get_platform().replace('.', '_').replace('-', '_')


def get_supported(versions=None, noarch=False):
def get_supported(versions=None, noarch=False, specificplatform=None):
"""Return a list of supported tags for each version specified in
`versions`.

:param versions: a list of string versions, of the form ["33", "32"],
or None. The first version will be assumed to support our ABI.
:param specificplatform: specify the exact platform you want valid
tags for, or None. If None, use the local system platform.
"""
supported = []

Expand Down Expand Up @@ -80,7 +82,7 @@ def get_supported(versions=None, noarch=False):
abis.append('none')

if not noarch:
arch = get_platform()
arch = specificplatform or get_platform()
if sys.platform == 'darwin':
# support macosx-10.6-intel on macosx-10.9-x86_64
match = _osx_arch_pat.match(arch)
Expand Down
26 changes: 26 additions & 0 deletions tests/functional/test_install_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,29 @@ def test_download_vcs_link(script):
in result.files_created
)
assert script.site_packages / 'piptestpackage' not in result.files_created


@pytest.mark.network
def test_pip_install_download_specify_platform(script, data):
"""
Test using "pip install --download --platform" to download a .whl archive
supported for a specific platform
"""
result = script.pip(
'install', '--no-index', '--find-links', data.find_links,
'--download', '.', '--platform', 'linux_x86_64', 'simplewheel'
)
assert (
Path('scratch') / 'simplewheel-2.0-py2.py3-none-any.whl'
in result.files_created
)

result = script.pip(
'install', '--no-index', '--find-links', data.find_links,
'--download', '.', '--platform', 'macosx_10_9_x86_64',
'requires_source'
)
assert (
Path('scratch') / 'requires_source-1.0-py2.py3-none-any.whl'
in result.files_created
)
13 changes: 6 additions & 7 deletions tests/unit/test_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def test_not_find_wheel_not_supported(self, data, monkeypatch):
[],
session=PipSession(),
)
finder.valid_tags = pip.pep425tags.supported_tags

with pytest.raises(DistributionNotFound):
finder.find_requirement(req, True)
Expand Down Expand Up @@ -215,11 +216,6 @@ def test_existing_over_wheel_priority(self, data):
with pytest.raises(BestVersionAlreadyInstalled):
finder.find_requirement(req, True)

@patch('pip.pep425tags.supported_tags', [
('pyT', 'none', 'TEST'),
('pyT', 'TEST', 'any'),
('pyT', 'none', 'any'),
])
def test_link_sorting(self):
"""
Test link sorting
Expand Down Expand Up @@ -248,9 +244,12 @@ def test_link_sorting(self):
Link('simple-1.0.tar.gz'),
),
]

finder = PackageFinder([], [], session=PipSession())

finder.valid_tags = [
('pyT', 'none', 'TEST'),
('pyT', 'TEST', 'any'),
('pyT', 'none', 'any'),
]
results = finder._sort_versions(links)
results2 = finder._sort_versions(reversed(links))

Expand Down