Skip to content

Commit 9e10fc6

Browse files
author
Mathew Jennings
committed
Add two new options to the pip install command that are meant to give the user more control when downloading packages.
The two new options are --platform and --supported-interpreter-version, and both enforce that the --download option is also specified, else a CommandError is raised. With the --platform option, a user can ask to download wheels of a different platform than that of the local machine running the command, which is designed as a utility for gathering dependencies and preparing a distribution. Because this option enforces that the --download option is also specified, it will never attempt to install wheels of an incorrect platform. With the --supported-interpreter-version option, a user can ask to download wheels that are explicitly compatible with a specific Python interpreter version, which is designed as a similar utility to the --package option.
1 parent a767bac commit 9e10fc6

File tree

6 files changed

+133
-22
lines changed

6 files changed

+133
-22
lines changed

dev-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ pytest-capturelog
55
pytest-cov
66
pytest-timeout
77
pytest-xdist
8-
mock
8+
mock<1.1
99
scripttest>=1.3
1010
https://github.com/pypa/virtualenv/archive/develop.zip#egg=virtualenv

pip/commands/install.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pip.req import RequirementSet
1515
from pip.basecommand import RequirementCommand
1616
from pip.locations import virtualenv_no_global, distutils_scheme
17+
from pip.pep425tags import get_platform
1718
from pip.index import PackageFinder
1819
from pip.exceptions import (
1920
InstallationError, CommandError, PreviousBuildDirError,
@@ -82,6 +83,33 @@ def __init__(self, *args, **kw):
8283
"regardless of what's already installed."),
8384
)
8485

86+
cmd_opts.add_option(
87+
'--platform',
88+
dest='platform',
89+
metavar='platform',
90+
default=get_platform(),
91+
help=("Specifically download wheels compatible with <platform> "
92+
"where the default is the platfrom of the local computer. "
93+
"This option may only be used if --download is also being "
94+
"used."),
95+
)
96+
97+
cmd_opts.add_option(
98+
'--interpreter-version',
99+
dest='interpreter_version',
100+
metavar='version',
101+
default='',
102+
help=("Specifically download wheels compatible with Python "
103+
"interpreter version <version>. If not specified, then the "
104+
"current system interpreter version is used. This option "
105+
"may only be used if --download is also being used. This "
106+
"is a stricter approach compared to the native search "
107+
"performed without this option specified: this does not "
108+
"accept previous minor versions of Python. It is meant to "
109+
"be used when you want to download packages that support an "
110+
"exact version of Python."),
111+
)
112+
85113
cmd_opts.add_option(cmdoptions.download_cache())
86114
cmd_opts.add_option(cmdoptions.src())
87115

@@ -175,7 +203,14 @@ def __init__(self, *args, **kw):
175203
self.parser.insert_option_group(0, index_opts)
176204
self.parser.insert_option_group(0, cmd_opts)
177205

178-
def _build_package_finder(self, options, index_urls, session):
206+
def _build_package_finder(
207+
self,
208+
options,
209+
index_urls,
210+
session,
211+
platform,
212+
desired_interp_versions
213+
):
179214
"""
180215
Create a package finder appropriate to this install command.
181216
This method is meant to be overridden by subclasses, not
@@ -192,6 +227,8 @@ def _build_package_finder(self, options, index_urls, session):
192227
allow_all_prereleases=options.pre,
193228
process_dependency_links=options.process_dependency_links,
194229
session=session,
230+
platform=platform,
231+
versions=desired_interp_versions,
195232
)
196233

197234
def run(self, options, args):
@@ -201,6 +238,26 @@ def run(self, options, args):
201238
if options.download_dir:
202239
options.ignore_installed = True
203240

241+
desired_platform = options.platform
242+
if desired_platform != get_platform() and not options.download_dir:
243+
raise CommandError(
244+
"Usage: Cannot use --platform without also using "
245+
"--download option."
246+
)
247+
248+
if options.interpreter_version:
249+
if options.download_dir:
250+
desired_interp_versions = [
251+
options.interpreter_version
252+
]
253+
else:
254+
raise CommandError(
255+
"Usage: Cannot use --interpreter-version "
256+
"without also using --download option."
257+
)
258+
else:
259+
desired_interp_versions = None
260+
204261
if options.build_dir:
205262
options.build_dir = os.path.abspath(options.build_dir)
206263

@@ -243,8 +300,13 @@ def run(self, options, args):
243300
)
244301

245302
with self._build_session(options) as session:
246-
247-
finder = self._build_package_finder(options, index_urls, session)
303+
finder = self._build_package_finder(
304+
options,
305+
index_urls,
306+
session,
307+
desired_platform,
308+
desired_interp_versions
309+
)
248310
build_delete = (not (options.no_clean or options.build_dir))
249311
wheel_cache = WheelCache(options.cache_dir, options.format_control)
250312
if options.cache_dir and not check_path_owner(options.cache_dir):

pip/index.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from pip.download import HAS_TLS, url_to_path, path_to_url
2929
from pip.models import PyPI
3030
from pip.wheel import Wheel, wheel_ext
31-
from pip.pep425tags import supported_tags, supported_tags_noarch, get_platform
31+
from pip.pep425tags import get_supported, get_platform
3232
from pip._vendor import html5lib, requests, pkg_resources, six
3333
from pip._vendor.packaging.version import parse as parse_version
3434
from pip._vendor.requests.exceptions import SSLError
@@ -103,12 +103,20 @@ def __init__(self, find_links, index_urls,
103103
allow_external=(), allow_unverified=(),
104104
allow_all_external=False, allow_all_prereleases=False,
105105
trusted_hosts=None, process_dependency_links=False,
106-
session=None, format_control=None):
106+
session=None, format_control=None, platform=None,
107+
versions=None):
107108
"""Create a PackageFinder.
108109
109110
:param format_control: A FormatControl object or None. Used to control
110111
the selection of source packages / binary packages when consulting
111112
the index and links.
113+
:param platform: A string or None. If None, searches for packages
114+
that are supported by the current system. Otherwise, will find
115+
packages that can be built on the platform passed in. It is
116+
understood that these packages will only be downloaded for
117+
distribution: they will not be built locally.
118+
:param versions: A list of strings or None. This is passed directly
119+
to pep425tags.py in the get_supported() method.
112120
"""
113121
if session is None:
114122
raise TypeError(
@@ -174,6 +182,19 @@ def __init__(self, find_links, index_urls,
174182
# The Session we'll use to make requests
175183
self.session = session
176184

185+
# The platform for which to find compatible packages
186+
self.platform = platform or get_platform()
187+
188+
# The valid tags to check potential found wheel candidates against
189+
self.valid_tags = get_supported(
190+
versions=versions,
191+
specificplatform=self.platform
192+
)
193+
self.valid_tags_noarch = get_supported(
194+
versions=versions,
195+
noarch=True
196+
)
197+
177198
# If we don't have TLS enabled, then WARN if anyplace we're looking
178199
# relies on TLS.
179200
if not HAS_TLS:
@@ -248,24 +269,24 @@ def _candidate_sort_key(self, candidate):
248269
If not finding wheels, then sorted by version only.
249270
If finding wheels, then the sort order is by version, then:
250271
1. existing installs
251-
2. wheels ordered via Wheel.support_index_min()
272+
2. wheels ordered via Wheel.support_index_min(self.valid_tags)
252273
3. source archives
253274
Note: it was considered to embed this logic into the Link
254275
comparison operators, but then different sdist links
255276
with the same version, would have to be considered equal
256277
"""
257-
support_num = len(supported_tags)
278+
support_num = len(self.valid_tags)
258279
if candidate.location == INSTALLED_VERSION:
259280
pri = 1
260281
elif candidate.location.is_wheel:
261282
# can raise InvalidWheelFilename
262283
wheel = Wheel(candidate.location.filename)
263-
if not wheel.supported():
284+
if not wheel.supported(self.valid_tags):
264285
raise UnsupportedWheel(
265286
"%s is not a supported wheel for this platform. It "
266287
"can't be sorted." % wheel.filename
267288
)
268-
pri = -(wheel.support_index_min())
289+
pri = -(wheel.support_index_min(self.valid_tags))
269290
else: # sdist
270291
pri = -(support_num)
271292
return (candidate.version, pri)
@@ -703,7 +724,7 @@ def _log_skipped_link(self, link, reason):
703724

704725
def _link_package_versions(self, link, search):
705726
"""Return an InstallationCandidate or None"""
706-
platform = get_platform()
727+
platform = self.platform
707728

708729
version = None
709730
if link.egg_fragment:
@@ -736,7 +757,8 @@ def _link_package_versions(self, link, search):
736757
self._log_skipped_link(
737758
link, 'wrong project name (not %s)' % search.supplied)
738759
return
739-
if not wheel.supported():
760+
761+
if not wheel.supported(self.valid_tags):
740762
self._log_skipped_link(
741763
link, 'it is not compatible with this Python')
742764
return
@@ -757,7 +779,7 @@ def _link_package_versions(self, link, search):
757779
urllib_parse.urlparse(
758780
comes_from.url
759781
).netloc.endswith(PyPI.netloc)):
760-
if not wheel.supported(tags=supported_tags_noarch):
782+
if not wheel.supported(tags=self.valid_tags_noarch):
761783
self._log_skipped_link(
762784
link,
763785
"it is a pypi-hosted binary "

pip/pep425tags.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ def get_platform():
3939
return distutils.util.get_platform().replace('.', '_').replace('-', '_')
4040

4141

42-
def get_supported(versions=None, noarch=False):
42+
def get_supported(versions=None, noarch=False, specificplatform=None):
4343
"""Return a list of supported tags for each version specified in
4444
`versions`.
4545
4646
:param versions: a list of string versions, of the form ["33", "32"],
4747
or None. The first version will be assumed to support our ABI.
48+
:param specificplatform: specify the exact platform you want valid
49+
tags for, or None. If None, use the local system platform.
4850
"""
4951
supported = []
5052

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

8284
if not noarch:
83-
arch = get_platform()
85+
arch = specificplatform or get_platform()
8486
if sys.platform == 'darwin':
8587
# support macosx-10.6-intel on macosx-10.9-x86_64
8688
match = _osx_arch_pat.match(arch)

tests/functional/test_install_download.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,29 @@ def test_download_vcs_link(script):
146146
in result.files_created
147147
)
148148
assert script.site_packages / 'piptestpackage' not in result.files_created
149+
150+
151+
@pytest.mark.network
152+
def test_pip_install_download_specify_platform(script, data):
153+
"""
154+
Test using "pip install --download --platform" to download a .whl archive
155+
supported for a specific platform
156+
"""
157+
result = script.pip(
158+
'install', '--no-index', '--find-links', data.find_links,
159+
'--download', '.', '--platform', 'linux_x86_64', 'simplewheel'
160+
)
161+
assert (
162+
Path('scratch') / 'simplewheel-2.0-py2.py3-none-any.whl'
163+
in result.files_created
164+
)
165+
166+
result = script.pip(
167+
'install', '--no-index', '--find-links', data.find_links,
168+
'--download', '.', '--platform', 'macosx_10_9_x86_64',
169+
'requires_source'
170+
)
171+
assert (
172+
Path('scratch') / 'requires_source-1.0-py2.py3-none-any.whl'
173+
in result.files_created
174+
)

tests/unit/test_finder.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def test_not_find_wheel_not_supported(self, data, monkeypatch):
154154
[],
155155
session=PipSession(),
156156
)
157+
finder.valid_tags = pip.pep425tags.supported_tags
157158

158159
with pytest.raises(DistributionNotFound):
159160
finder.find_requirement(req, True)
@@ -215,11 +216,6 @@ def test_existing_over_wheel_priority(self, data):
215216
with pytest.raises(BestVersionAlreadyInstalled):
216217
finder.find_requirement(req, True)
217218

218-
@patch('pip.pep425tags.supported_tags', [
219-
('pyT', 'none', 'TEST'),
220-
('pyT', 'TEST', 'any'),
221-
('pyT', 'none', 'any'),
222-
])
223219
def test_link_sorting(self):
224220
"""
225221
Test link sorting
@@ -248,9 +244,12 @@ def test_link_sorting(self):
248244
Link('simple-1.0.tar.gz'),
249245
),
250246
]
251-
252247
finder = PackageFinder([], [], session=PipSession())
253-
248+
finder.valid_tags = [
249+
('pyT', 'none', 'TEST'),
250+
('pyT', 'TEST', 'any'),
251+
('pyT', 'none', 'any'),
252+
]
254253
results = finder._sort_versions(links)
255254
results2 = finder._sort_versions(reversed(links))
256255

0 commit comments

Comments
 (0)