Skip to content

Commit 20127bf

Browse files
authored
Merge pull request #5404 from sixninetynine/feature/platforms_for_target
Open up plat/abi/impl options to `install --target`
2 parents 207a239 + cddcb14 commit 20127bf

File tree

7 files changed

+186
-72
lines changed

7 files changed

+186
-72
lines changed

news/5355.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allows dist options (--abi, --python-version, --platform, --implementation) when installing with --target

src/pip/_internal/cli/cmdoptions.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from functools import partial
1414
from optparse import SUPPRESS_HELP, Option, OptionGroup
1515

16+
from pip._internal.exceptions import CommandError
1617
from pip._internal.index import (
1718
FormatControl, fmt_ctl_handle_mutual_exclude, fmt_ctl_no_binary,
1819
)
@@ -60,6 +61,45 @@ def getname(n):
6061
)
6162

6263

64+
def check_dist_restriction(options, check_target=False):
65+
"""Function for determining if custom platform options are allowed.
66+
67+
:param options: The OptionParser options.
68+
:param check_target: Whether or not to check if --target is being used.
69+
"""
70+
dist_restriction_set = any([
71+
options.python_version,
72+
options.platform,
73+
options.abi,
74+
options.implementation,
75+
])
76+
77+
binary_only = FormatControl(set(), {':all:'})
78+
sdist_dependencies_allowed = (
79+
options.format_control != binary_only and
80+
not options.ignore_dependencies
81+
)
82+
83+
# Installations or downloads using dist restrictions must not combine
84+
# source distributions and dist-specific wheels, as they are not
85+
# gauranteed to be locally compatible.
86+
if dist_restriction_set and sdist_dependencies_allowed:
87+
raise CommandError(
88+
"When restricting platform and interpreter constraints using "
89+
"--python-version, --platform, --abi, or --implementation, "
90+
"either --no-deps must be set, or --only-binary=:all: must be "
91+
"set and --no-binary must not be set (or must be set to "
92+
":none:)."
93+
)
94+
95+
if check_target:
96+
if dist_restriction_set and not options.target_dir:
97+
raise CommandError(
98+
"Can not use any platform or abi specific options unless "
99+
"installing via '--target'"
100+
)
101+
102+
63103
###########
64104
# options #
65105
###########
@@ -406,6 +446,61 @@ def only_binary():
406446
)
407447

408448

449+
platform = partial(
450+
Option,
451+
'--platform',
452+
dest='platform',
453+
metavar='platform',
454+
default=None,
455+
help=("Only use wheels compatible with <platform>. "
456+
"Defaults to the platform of the running system."),
457+
)
458+
459+
460+
python_version = partial(
461+
Option,
462+
'--python-version',
463+
dest='python_version',
464+
metavar='python_version',
465+
default=None,
466+
help=("Only use wheels compatible with Python "
467+
"interpreter version <version>. If not specified, then the "
468+
"current system interpreter minor version is used. A major "
469+
"version (e.g. '2') can be specified to match all "
470+
"minor revs of that major version. A minor version "
471+
"(e.g. '34') can also be specified."),
472+
)
473+
474+
475+
implementation = partial(
476+
Option,
477+
'--implementation',
478+
dest='implementation',
479+
metavar='implementation',
480+
default=None,
481+
help=("Only use wheels compatible with Python "
482+
"implementation <implementation>, e.g. 'pp', 'jy', 'cp', "
483+
" or 'ip'. If not specified, then the current "
484+
"interpreter implementation is used. Use 'py' to force "
485+
"implementation-agnostic wheels."),
486+
)
487+
488+
489+
abi = partial(
490+
Option,
491+
'--abi',
492+
dest='abi',
493+
metavar='abi',
494+
default=None,
495+
help=("Only use wheels compatible with Python "
496+
"abi <abi>, e.g. 'pypy_41'. If not specified, then the "
497+
"current interpreter abi tag is used. Generally "
498+
"you will need to specify --implementation, "
499+
"--platform, and --python-version when using "
500+
"this option."),
501+
)
502+
503+
409504
def prefer_binary():
410505
return Option(
411506
"--prefer-binary",

src/pip/_internal/commands/download.py

Lines changed: 5 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
from pip._internal.cli import cmdoptions
77
from pip._internal.cli.base_command import RequirementCommand
8-
from pip._internal.exceptions import CommandError
9-
from pip._internal.index import FormatControl
108
from pip._internal.operations.prepare import RequirementPreparer
119
from pip._internal.req import RequirementSet
1210
from pip._internal.req.req_tracker import RequirementTracker
@@ -69,52 +67,10 @@ def __init__(self, *args, **kw):
6967
help=("Download packages into <dir>."),
7068
)
7169

72-
cmd_opts.add_option(
73-
'--platform',
74-
dest='platform',
75-
metavar='platform',
76-
default=None,
77-
help=("Only download wheels compatible with <platform>. "
78-
"Defaults to the platform of the running system."),
79-
)
80-
81-
cmd_opts.add_option(
82-
'--python-version',
83-
dest='python_version',
84-
metavar='python_version',
85-
default=None,
86-
help=("Only download wheels compatible with Python "
87-
"interpreter version <version>. If not specified, then the "
88-
"current system interpreter minor version is used. A major "
89-
"version (e.g. '2') can be specified to match all "
90-
"minor revs of that major version. A minor version "
91-
"(e.g. '34') can also be specified."),
92-
)
93-
94-
cmd_opts.add_option(
95-
'--implementation',
96-
dest='implementation',
97-
metavar='implementation',
98-
default=None,
99-
help=("Only download wheels compatible with Python "
100-
"implementation <implementation>, e.g. 'pp', 'jy', 'cp', "
101-
" or 'ip'. If not specified, then the current "
102-
"interpreter implementation is used. Use 'py' to force "
103-
"implementation-agnostic wheels."),
104-
)
105-
106-
cmd_opts.add_option(
107-
'--abi',
108-
dest='abi',
109-
metavar='abi',
110-
default=None,
111-
help=("Only download wheels compatible with Python "
112-
"abi <abi>, e.g. 'pypy_41'. If not specified, then the "
113-
"current interpreter abi tag is used. Generally "
114-
"you will need to specify --implementation, "
115-
"--platform, and --python-version when using "
116-
"this option."),
117-
)
70+
cmd_opts.add_option(cmdoptions.platform())
71+
cmd_opts.add_option(cmdoptions.python_version())
72+
cmd_opts.add_option(cmdoptions.implementation())
73+
cmd_opts.add_option(cmdoptions.abi())
11874

11975
index_opts = cmdoptions.make_option_group(
12076
cmdoptions.index_group,
@@ -135,25 +91,7 @@ def run(self, options, args):
13591
else:
13692
python_versions = None
13793

138-
dist_restriction_set = any([
139-
options.python_version,
140-
options.platform,
141-
options.abi,
142-
options.implementation,
143-
])
144-
binary_only = FormatControl(set(), {':all:'})
145-
no_sdist_dependencies = (
146-
options.format_control != binary_only and
147-
not options.ignore_dependencies
148-
)
149-
if dist_restriction_set and no_sdist_dependencies:
150-
raise CommandError(
151-
"When restricting platform and interpreter constraints using "
152-
"--python-version, --platform, --abi, or --implementation, "
153-
"either --no-deps must be set, or --only-binary=:all: must be "
154-
"set and --no-binary must not be set (or must be set to "
155-
":none:)."
156-
)
94+
cmdoptions.check_dist_restriction(options)
15795

15896
options.src_dir = os.path.abspath(options.src_dir)
15997
options.download_dir = normalize_path(options.download_dir)

src/pip/_internal/commands/install.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ def __init__(self, *args, **kw):
8383
'<dir>. Use --upgrade to replace existing packages in <dir> '
8484
'with new versions.'
8585
)
86+
cmd_opts.add_option(cmdoptions.platform())
87+
cmd_opts.add_option(cmdoptions.python_version())
88+
cmd_opts.add_option(cmdoptions.implementation())
89+
cmd_opts.add_option(cmdoptions.abi())
90+
8691
cmd_opts.add_option(
8792
'--user',
8893
dest='use_user_site',
@@ -204,14 +209,20 @@ def __init__(self, *args, **kw):
204209

205210
def run(self, options, args):
206211
cmdoptions.check_install_build_global(options)
207-
208212
upgrade_strategy = "to-satisfy-only"
209213
if options.upgrade:
210214
upgrade_strategy = options.upgrade_strategy
211215

212216
if options.build_dir:
213217
options.build_dir = os.path.abspath(options.build_dir)
214218

219+
cmdoptions.check_dist_restriction(options, check_target=True)
220+
221+
if options.python_version:
222+
python_versions = [options.python_version]
223+
else:
224+
python_versions = None
225+
215226
options.src_dir = os.path.abspath(options.src_dir)
216227
install_options = options.install_options or []
217228
if options.use_user_site:
@@ -246,7 +257,14 @@ def run(self, options, args):
246257
global_options = options.global_options or []
247258

248259
with self._build_session(options) as session:
249-
finder = self._build_package_finder(options, session)
260+
finder = self._build_package_finder(
261+
options=options,
262+
session=session,
263+
platform=options.platform,
264+
python_versions=python_versions,
265+
abi=options.abi,
266+
implementation=options.implementation,
267+
)
250268
build_delete = (not (options.no_clean or options.build_dir))
251269
wheel_cache = WheelCache(options.cache_dir, options.format_control)
252270

@@ -266,6 +284,7 @@ def run(self, options, args):
266284
) as directory:
267285
requirement_set = RequirementSet(
268286
require_hashes=options.require_hashes,
287+
check_supported_wheels=not options.target_dir,
269288
)
270289

271290
try:

src/pip/_internal/req/req_set.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212

1313
class RequirementSet(object):
1414

15-
def __init__(self, require_hashes=False):
15+
def __init__(self, require_hashes=False, check_supported_wheels=True):
1616
"""Create a RequirementSet.
1717
"""
1818

1919
self.requirements = OrderedDict()
2020
self.require_hashes = require_hashes
21+
self.check_supported_wheels = check_supported_wheels
2122

2223
# Mapping of alias: real_name
2324
self.requirement_aliases = {}
@@ -65,7 +66,7 @@ def add_requirement(self, install_req, parent_req_name=None,
6566
# environment markers.
6667
if install_req.link and install_req.link.is_wheel:
6768
wheel = Wheel(install_req.link.filename)
68-
if not wheel.supported():
69+
if self.check_supported_wheels and not wheel.supported():
6970
raise InstallationError(
7071
"%s is not a supported wheel on this platform." %
7172
wheel.filename
1.79 KB
Binary file not shown.

tests/functional/test_install.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99

1010
from pip._internal import pep425tags
11-
from pip._internal.cli.status_codes import ERROR
11+
from pip._internal.cli.status_codes import ERROR, SUCCESS
1212
from pip._internal.models.index import PyPI, TestPyPI
1313
from pip._internal.utils.misc import rmtree
1414
from tests.lib import (
@@ -679,6 +679,66 @@ def test_install_package_with_target(script):
679679
assert singlemodule_py in result.files_updated, str(result)
680680

681681

682+
def test_install_nonlocal_compatible_wheel(script, data):
683+
target_dir = script.scratch_path / 'target'
684+
685+
# Test install with --target
686+
result = script.pip(
687+
'install',
688+
'-t', target_dir,
689+
'--no-index', '--find-links', data.find_links,
690+
'--only-binary=:all:',
691+
'--python', '3',
692+
'--platform', 'fakeplat',
693+
'--abi', 'fakeabi',
694+
'simplewheel',
695+
)
696+
assert result.returncode == SUCCESS
697+
698+
distinfo = Path('scratch') / 'target' / 'simplewheel-2.0-1.dist-info'
699+
assert distinfo in result.files_created
700+
701+
# Test install without --target
702+
result = script.pip(
703+
'install',
704+
'--no-index', '--find-links', data.find_links,
705+
'--only-binary=:all:',
706+
'--python', '3',
707+
'--platform', 'fakeplat',
708+
'--abi', 'fakeabi',
709+
'simplewheel',
710+
expect_error=True
711+
)
712+
assert result.returncode == ERROR
713+
714+
715+
def test_install_nonlocal_compatible_wheel_path(script, data):
716+
target_dir = script.scratch_path / 'target'
717+
718+
# Test a full path requirement
719+
result = script.pip(
720+
'install',
721+
'-t', target_dir,
722+
'--no-index',
723+
'--only-binary=:all:',
724+
Path(data.packages) / 'simplewheel-2.0-py3-fakeabi-fakeplat.whl'
725+
)
726+
assert result.returncode == SUCCESS
727+
728+
distinfo = Path('scratch') / 'target' / 'simplewheel-2.0.dist-info'
729+
assert distinfo in result.files_created
730+
731+
# Test a full path requirement (without --target)
732+
result = script.pip(
733+
'install',
734+
'--no-index',
735+
'--only-binary=:all:',
736+
Path(data.packages) / 'simplewheel-2.0-py3-fakeabi-fakeplat.whl',
737+
expect_error=True
738+
)
739+
assert result.returncode == ERROR
740+
741+
682742
def test_install_with_target_and_scripts_no_warning(script, common_wheels):
683743
"""
684744
Test that installing with --target does not trigger the "script not

0 commit comments

Comments
 (0)