Skip to content

Commit d67d98d

Browse files
DanielShaulovpradyunsg
authored andcommitted
Add a --prefer-binary flag. (pypa#5370)
The flag makes pip prefer an older but valid binary distributions over a newer source distributions. Fixes pypa#3785.
1 parent df45aaf commit d67d98d

File tree

8 files changed

+82
-2
lines changed

8 files changed

+82
-2
lines changed

news/3785.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Introduce a new --prefer-binary flag, to prefer older wheels over newer source packages.

src/pip/_internal/basecommand.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,4 +370,5 @@ def _build_package_finder(self, options, session,
370370
versions=python_versions,
371371
abi=abi,
372372
implementation=implementation,
373+
prefer_binary=options.prefer_binary,
373374
)

src/pip/_internal/cmdoptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,16 @@ def only_binary():
406406
)
407407

408408

409+
def prefer_binary():
410+
return Option(
411+
"--prefer-binary",
412+
dest="prefer_binary",
413+
action="store_true",
414+
default=False,
415+
help="Prefer older binary packages over newer source packages."
416+
)
417+
418+
409419
cache_dir = partial(
410420
Option,
411421
"--cache-dir",

src/pip/_internal/commands/download.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def __init__(self, *args, **kw):
5252
cmd_opts.add_option(cmdoptions.global_options())
5353
cmd_opts.add_option(cmdoptions.no_binary())
5454
cmd_opts.add_option(cmdoptions.only_binary())
55+
cmd_opts.add_option(cmdoptions.prefer_binary())
5556
cmd_opts.add_option(cmdoptions.src())
5657
cmd_opts.add_option(cmdoptions.pre())
5758
cmd_opts.add_option(cmdoptions.no_clean())

src/pip/_internal/commands/install.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ def __init__(self, *args, **kw):
183183

184184
cmd_opts.add_option(cmdoptions.no_binary())
185185
cmd_opts.add_option(cmdoptions.only_binary())
186+
cmd_opts.add_option(cmdoptions.prefer_binary())
186187
cmd_opts.add_option(cmdoptions.no_clean())
187188
cmd_opts.add_option(cmdoptions.require_hashes())
188189
cmd_opts.add_option(cmdoptions.progress_bar())

src/pip/_internal/commands/wheel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(self, *args, **kw):
5757
)
5858
cmd_opts.add_option(cmdoptions.no_binary())
5959
cmd_opts.add_option(cmdoptions.only_binary())
60+
cmd_opts.add_option(cmdoptions.prefer_binary())
6061
cmd_opts.add_option(
6162
'--build-option',
6263
dest='build_options',

src/pip/_internal/index.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ class PackageFinder(object):
108108
def __init__(self, find_links, index_urls, allow_all_prereleases=False,
109109
trusted_hosts=None, process_dependency_links=False,
110110
session=None, format_control=None, platform=None,
111-
versions=None, abi=None, implementation=None):
111+
versions=None, abi=None, implementation=None,
112+
prefer_binary=False):
112113
"""Create a PackageFinder.
113114
114115
:param format_control: A FormatControl object or None. Used to control
@@ -176,6 +177,9 @@ def __init__(self, find_links, index_urls, allow_all_prereleases=False,
176177
impl=implementation,
177178
)
178179

180+
# Do we prefer old, but valid, binary dist over new source dist
181+
self.prefer_binary = prefer_binary
182+
179183
# If we don't have TLS enabled, then WARN if anyplace we're looking
180184
# relies on TLS.
181185
if not HAS_TLS:
@@ -275,12 +279,14 @@ def _candidate_sort_key(self, candidate):
275279
1. existing installs
276280
2. wheels ordered via Wheel.support_index_min(self.valid_tags)
277281
3. source archives
282+
If prefer_binary was set, then all wheels are sorted above sources.
278283
Note: it was considered to embed this logic into the Link
279284
comparison operators, but then different sdist links
280285
with the same version, would have to be considered equal
281286
"""
282287
support_num = len(self.valid_tags)
283288
build_tag = tuple()
289+
binary_preference = 0
284290
if candidate.location.is_wheel:
285291
# can raise InvalidWheelFilename
286292
wheel = Wheel(candidate.location.filename)
@@ -289,14 +295,16 @@ def _candidate_sort_key(self, candidate):
289295
"%s is not a supported wheel for this platform. It "
290296
"can't be sorted." % wheel.filename
291297
)
298+
if self.prefer_binary:
299+
binary_preference = 1
292300
pri = -(wheel.support_index_min(self.valid_tags))
293301
if wheel.build_tag is not None:
294302
match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
295303
build_tag_groups = match.groups()
296304
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
297305
else: # sdist
298306
pri = -(support_num)
299-
return (candidate.version, build_tag, pri)
307+
return (binary_preference, candidate.version, build_tag, pri)
300308

301309
def _validate_secure_origin(self, logger, location):
302310
# Determine if this url used a secure transport mechanism

tests/functional/test_download.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,3 +602,60 @@ def test_download_exit_status_code_when_blank_requirements_file(script):
602602
"""
603603
script.scratch_path.join("blank.txt").write("\n")
604604
script.pip('download', '-r', 'blank.txt')
605+
606+
607+
def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data):
608+
fake_wheel(data, 'source-0.8-py2.py3-none-any.whl')
609+
result = script.pip(
610+
'download',
611+
'--prefer-binary',
612+
'--no-index',
613+
'-f', data.packages,
614+
'-d', '.', 'source'
615+
)
616+
assert (
617+
Path('scratch') / 'source-0.8-py2.py3-none-any.whl'
618+
in result.files_created
619+
)
620+
assert (
621+
Path('scratch') / 'source-1.0.tar.gz'
622+
not in result.files_created
623+
)
624+
625+
626+
def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(script, data):
627+
fake_wheel(data, 'source-0.8-py2.py3-none-any.whl')
628+
script.scratch_path.join("test-req.txt").write(textwrap.dedent("""
629+
source>0.9
630+
"""))
631+
632+
result = script.pip(
633+
'download',
634+
'--prefer-binary',
635+
'--no-index',
636+
'-f', data.packages,
637+
'-d', '.',
638+
'-r', script.scratch_path / 'test-req.txt'
639+
)
640+
assert (
641+
Path('scratch') / 'source-1.0.tar.gz'
642+
in result.files_created
643+
)
644+
assert (
645+
Path('scratch') / 'source-0.8-py2.py3-none-any.whl'
646+
not in result.files_created
647+
)
648+
649+
650+
def test_download_prefer_binary_when_only_tarball_exists(script, data):
651+
result = script.pip(
652+
'download',
653+
'--prefer-binary',
654+
'--no-index',
655+
'-f', data.packages,
656+
'-d', '.', 'source'
657+
)
658+
assert (
659+
Path('scratch') / 'source-1.0.tar.gz'
660+
in result.files_created
661+
)

0 commit comments

Comments
 (0)