Skip to content

Commit ff1206f

Browse files
committed
Merge pull request pypa#2699 from rbtcollins/issue-2675
Issue 2675
2 parents 31eb67d + 6aec23c commit ff1206f

22 files changed

+365
-42
lines changed

CHANGES.txt

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
* Build Wheels prior to installing from sdist, caching them in the pip cache
2626
directory to speed up subsequent installs. (:pull:`2618`)
2727

28+
* Allow fine grained control over the use of wheels and source builds.
29+
(:pull:`2699`)
30+
2831
**6.1.1 (2015-04-07)**
2932

3033
* No longer ignore dependencies which have been added to the standard library,

docs/reference/pip_install.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ Additionally, the following Package Index Options are supported:
112112
* :ref:`--allow-external <--allow-external>`
113113
* :ref:`--allow-all-external <--allow-external>`
114114
* :ref:`--allow-unverified <--allow-unverified>`
115-
* :ref:`--no-use-wheel <install_--no-use-wheel>`
115+
* :ref:`--no-binary <install_--no-binary>`
116+
* :ref:`--only-binary <install_--only-binary>`
116117

117118
For example, to specify :ref:`--no-index <--no-index>` and 2 :ref:`--find-links <--find-links>` locations:
118119

docs/user_guide.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ to building and installing from source archives. For more information, see the
123123
`PEP425 <http://www.python.org/dev/peps/pep-0425>`_
124124

125125
Pip prefers Wheels where they are available. To disable this, use the
126-
:ref:`--no-use-wheel <install_--no-use-wheel>` flag for :ref:`pip install`.
126+
:ref:`--no-binary <install_--no-binary>` flag for :ref:`pip install`.
127127

128128
If no satisfactory wheels are found, pip will default to finding source archives.
129129

pip/cmdoptions.py

+55-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
from functools import partial
1313
from optparse import OptionGroup, SUPPRESS_HELP, Option
14-
from pip.index import PyPI
14+
15+
from pip.index import (
16+
PyPI, FormatControl, fmt_ctl_handle_mutual_exclude, fmt_ctl_no_use_wheel)
1517
from pip.locations import CA_BUNDLE_PATH, USER_CACHE_DIR, src_prefix
1618

1719

@@ -27,6 +29,12 @@ def make_option_group(group, parser):
2729
return option_group
2830

2931

32+
def resolve_wheel_no_use_binary(options):
33+
if not options.use_wheel:
34+
control = options.format_control
35+
fmt_ctl_no_use_wheel(control)
36+
37+
3038
###########
3139
# options #
3240
###########
@@ -339,6 +347,7 @@ def editable():
339347
'The default for global installs is "<current dir>/src".'
340348
)
341349

350+
# XXX: deprecated, remove in 9.0
342351
use_wheel = partial(
343352
Option,
344353
'--use-wheel',
@@ -354,9 +363,53 @@ def editable():
354363
action='store_false',
355364
default=True,
356365
help=('Do not Find and prefer wheel archives when searching indexes and '
357-
'find-links locations.'),
366+
'find-links locations. DEPRECATED in favour of --no-binary.'),
358367
)
359368

369+
370+
def _get_format_control(values, option):
371+
"""Get a format_control object."""
372+
return getattr(values, option.dest)
373+
374+
375+
def _handle_no_binary(option, opt_str, value, parser):
376+
existing = getattr(parser.values, option.dest)
377+
fmt_ctl_handle_mutual_exclude(
378+
value, existing.no_binary, existing.only_binary)
379+
380+
381+
def _handle_only_binary(option, opt_str, value, parser):
382+
existing = getattr(parser.values, option.dest)
383+
fmt_ctl_handle_mutual_exclude(
384+
value, existing.only_binary, existing.no_binary)
385+
386+
387+
def no_binary():
388+
return Option(
389+
"--no-binary", dest="format_control", action="callback",
390+
callback=_handle_no_binary, type="str",
391+
default=FormatControl(set(), set()),
392+
help="Do not use binary packages. Can be supplied multiple times, and "
393+
"each time adds to the existing value. Accepts either :all: to "
394+
"disable all binary packages, :none: to empty the set, or one or "
395+
"more package names with commas between them. Note that some "
396+
"packages are tricky to compile and may fail to install when "
397+
"this option is used on them.")
398+
399+
400+
def only_binary():
401+
return Option(
402+
"--only-binary", dest="format_control", action="callback",
403+
callback=_handle_only_binary, type="str",
404+
default=FormatControl(set(), set()),
405+
help="Do not use source packages. Can be supplied multiple times, and "
406+
"each time adds to the existing value. Accepts either :all: to "
407+
"disable all source packages, :none: to empty the set, or one or "
408+
"more package names with commas between them. Packages without "
409+
"binary distributions will fail to install when this option is "
410+
"used on them.")
411+
412+
360413
cache_dir = partial(
361414
Option,
362415
"--cache-dir",

pip/commands/freeze.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import sys
44

5+
import pip
56
from pip.basecommand import Command
67
from pip.operations.freeze import freeze
78
from pip.wheel import WheelCache
@@ -55,7 +56,8 @@ def __init__(self, *args, **kw):
5556
self.parser.insert_option_group(0, self.cmd_opts)
5657

5758
def run(self, options, args):
58-
wheel_cache = WheelCache(options.cache_dir)
59+
format_control = pip.index.FormatControl(set(), set())
60+
wheel_cache = WheelCache(options.cache_dir, format_control)
5961
freeze_kwargs = dict(
6062
requirement=options.requirement,
6163
find_links=options.find_links,

pip/commands/install.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ def __init__(self, *args, **kw):
153153

154154
cmd_opts.add_option(cmdoptions.use_wheel())
155155
cmd_opts.add_option(cmdoptions.no_use_wheel())
156+
cmd_opts.add_option(cmdoptions.no_binary())
157+
cmd_opts.add_option(cmdoptions.only_binary())
156158

157159
cmd_opts.add_option(
158160
'--pre',
@@ -179,8 +181,8 @@ def _build_package_finder(self, options, index_urls, session):
179181
"""
180182
return PackageFinder(
181183
find_links=options.find_links,
184+
format_control=options.format_control,
182185
index_urls=index_urls,
183-
use_wheel=options.use_wheel,
184186
allow_external=options.allow_external,
185187
allow_unverified=options.allow_unverified,
186188
allow_all_external=options.allow_all_external,
@@ -191,6 +193,7 @@ def _build_package_finder(self, options, index_urls, session):
191193
)
192194

193195
def run(self, options, args):
196+
cmdoptions.resolve_wheel_no_use_binary(options)
194197

195198
if options.download_dir:
196199
options.ignore_installed = True
@@ -239,7 +242,7 @@ def run(self, options, args):
239242

240243
finder = self._build_package_finder(options, index_urls, session)
241244
build_delete = (not (options.no_clean or options.build_dir))
242-
wheel_cache = WheelCache(options.cache_dir)
245+
wheel_cache = WheelCache(options.cache_dir, options.format_control)
243246
with BuildDirectory(options.build_dir,
244247
delete=build_delete) as build_dir:
245248
requirement_set = RequirementSet(

pip/commands/list.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from pip.basecommand import Command
88
from pip.exceptions import DistributionNotFound
9-
from pip.index import PackageFinder, Search
9+
from pip.index import FormatControl, fmt_ctl_formats, PackageFinder, Search
1010
from pip.req import InstallRequirement
1111
from pip.utils import get_installed_distributions, dist_is_editable
1212
from pip.wheel import WheelCache
@@ -131,7 +131,8 @@ def find_packages_latest_versions(self, options):
131131
user_only=options.user,
132132
include_editables=False,
133133
)
134-
wheel_cache = WheelCache(options.cache_dir)
134+
format_control = FormatControl(set(), set())
135+
wheel_cache = WheelCache(options.cache_dir, format_control)
135136
for dist in installed_packages:
136137
req = InstallRequirement.from_line(
137138
dist.key, None, isolated=options.isolated_mode,
@@ -148,10 +149,12 @@ def find_packages_latest_versions(self, options):
148149
except DistributionNotFound:
149150
continue
150151
else:
152+
canonical_name = pkg_resources.safe_name(req.name).lower()
153+
formats = fmt_ctl_formats(format_control, canonical_name)
151154
search = Search(
152155
req.name,
153-
pkg_resources.safe_name(req.name).lower(),
154-
["source", "binary"])
156+
canonical_name,
157+
formats)
155158
remote_version = finder._link_package_versions(
156159
link, search).version
157160
if link.is_wheel:

pip/commands/uninstall.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import absolute_import
22

3+
import pip
34
from pip.wheel import WheelCache
45
from pip.req import InstallRequirement, RequirementSet, parse_requirements
56
from pip.basecommand import Command
@@ -43,7 +44,8 @@ def __init__(self, *args, **kw):
4344

4445
def run(self, options, args):
4546
with self._build_session(options) as session:
46-
wheel_cache = WheelCache(options.cache_dir)
47+
format_control = pip.index.FormatControl(set(), set())
48+
wheel_cache = WheelCache(options.cache_dir, format_control)
4749
requirement_set = RequirementSet(
4850
build_dir=None,
4951
src_dir=None,

pip/commands/wheel.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ def __init__(self, *args, **kw):
6161
)
6262
cmd_opts.add_option(cmdoptions.use_wheel())
6363
cmd_opts.add_option(cmdoptions.no_use_wheel())
64+
cmd_opts.add_option(cmdoptions.no_binary())
65+
cmd_opts.add_option(cmdoptions.only_binary())
6466
cmd_opts.add_option(
6567
'--build-option',
6668
dest='build_options',
@@ -122,6 +124,7 @@ def check_required_packages(self):
122124

123125
def run(self, options, args):
124126
self.check_required_packages()
127+
cmdoptions.resolve_wheel_no_use_binary(options)
125128

126129
index_urls = [options.index_url] + options.extra_index_urls
127130
if options.no_index:
@@ -143,8 +146,8 @@ def run(self, options, args):
143146

144147
finder = PackageFinder(
145148
find_links=options.find_links,
149+
format_control=options.format_control,
146150
index_urls=index_urls,
147-
use_wheel=options.use_wheel,
148151
allow_external=options.allow_external,
149152
allow_unverified=options.allow_unverified,
150153
allow_all_external=options.allow_all_external,
@@ -155,7 +158,7 @@ def run(self, options, args):
155158
)
156159

157160
build_delete = (not (options.no_clean or options.build_dir))
158-
wheel_cache = WheelCache(options.cache_dir)
161+
wheel_cache = WheelCache(options.cache_dir, options.format_control)
159162
with BuildDirectory(options.build_dir,
160163
delete=build_delete) as build_dir:
161164
requirement_set = RequirementSet(

pip/index.py

+71-12
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from pip._vendor.requests.exceptions import SSLError
3535

3636

37-
__all__ = ['PackageFinder']
37+
__all__ = ['FormatControl', 'fmt_ctl_handle_mutual_exclude', 'PackageFinder']
3838

3939

4040
# Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC)
@@ -97,14 +97,20 @@ class PackageFinder(object):
9797
"""This finds packages.
9898
9999
This is meant to match easy_install's technique for looking for
100-
packages, by reading pages and looking for appropriate links
100+
packages, by reading pages and looking for appropriate links.
101101
"""
102102

103103
def __init__(self, find_links, index_urls,
104-
use_wheel=True, allow_external=(), allow_unverified=(),
104+
allow_external=(), allow_unverified=(),
105105
allow_all_external=False, allow_all_prereleases=False,
106106
trusted_hosts=None, process_dependency_links=False,
107-
session=None):
107+
session=None, format_control=None):
108+
"""Create a PackageFinder.
109+
110+
:param format_control: A FormatControl object or None. Used to control
111+
the selection of source packages / binary packages when consulting
112+
the index and links.
113+
"""
108114
if session is None:
109115
raise TypeError(
110116
"PackageFinder() missing 1 required keyword argument: "
@@ -130,7 +136,7 @@ def __init__(self, find_links, index_urls,
130136
# These are boring links that have already been logged somehow:
131137
self.logged_links = set()
132138

133-
self.use_wheel = use_wheel
139+
self.format_control = format_control or FormatControl(set(), set())
134140

135141
# Do we allow (safe and verifiable) externally hosted files?
136142
self.allow_external = set(normalize_name(n) for n in allow_external)
@@ -413,13 +419,9 @@ def _find_all_versions(self, project_name):
413419
for location in url_locations:
414420
logger.debug('* %s', location)
415421

416-
formats = set(["source"])
417-
if self.use_wheel:
418-
formats.add("binary")
419-
search = Search(
420-
project_name.lower(),
421-
pkg_resources.safe_name(project_name).lower(),
422-
frozenset(formats))
422+
canonical_name = pkg_resources.safe_name(project_name).lower()
423+
formats = fmt_ctl_formats(self.format_control, canonical_name)
424+
search = Search(project_name.lower(), canonical_name, formats)
423425
find_links_versions = self._package_versions(
424426
# We trust every directly linked archive in find_links
425427
(Link(url, '-f', trusted=True) for url in self.find_links),
@@ -686,6 +688,7 @@ def _link_package_versions(self, link, search):
686688
version = None
687689
if link.egg_fragment:
688690
egg_info = link.egg_fragment
691+
ext = link.ext
689692
else:
690693
egg_info, ext = link.splitext()
691694
if not ext:
@@ -743,6 +746,12 @@ def _link_package_versions(self, link, search):
743746
return
744747
version = wheel.version
745748

749+
# This should be up by the search.ok_binary check, but see issue 2700.
750+
if "source" not in search.formats and ext != wheel_ext:
751+
self._log_skipped_link(
752+
link, 'No sources permitted for %s' % search.supplied)
753+
return
754+
746755
if not version:
747756
version = egg_info_matches(egg_info, search.supplied, link)
748757
if version is None:
@@ -1192,6 +1201,56 @@ def is_wheel(self):
11921201
INSTALLED_VERSION = Link(Inf)
11931202

11941203

1204+
FormatControl = namedtuple('FormatControl', 'no_binary only_binary')
1205+
"""This object has two fields, no_binary and only_binary.
1206+
1207+
If a field is falsy, it isn't set. If it is {':all:'}, it should match all
1208+
packages except those listed in the other field. Only one field can be set
1209+
to {':all:'} at a time. The rest of the time exact package name matches
1210+
are listed, with any given package only showing up in one field at a time.
1211+
"""
1212+
1213+
1214+
def fmt_ctl_handle_mutual_exclude(value, target, other):
1215+
new = value.split(',')
1216+
while ':all:' in new:
1217+
other.clear()
1218+
target.clear()
1219+
target.add(':all:')
1220+
del new[:new.index(':all:') + 1]
1221+
if ':none:' not in new:
1222+
# Without a none, we want to discard everything as :all: covers it
1223+
return
1224+
for name in new:
1225+
if name == ':none:':
1226+
target.clear()
1227+
continue
1228+
name = pkg_resources.safe_name(name).lower()
1229+
other.discard(name)
1230+
target.add(name)
1231+
1232+
1233+
def fmt_ctl_formats(fmt_ctl, canonical_name):
1234+
result = set(["binary", "source"])
1235+
if canonical_name in fmt_ctl.only_binary:
1236+
result.discard('source')
1237+
elif canonical_name in fmt_ctl.no_binary:
1238+
result.discard('binary')
1239+
elif ':all:' in fmt_ctl.only_binary:
1240+
result.discard('source')
1241+
elif ':all:' in fmt_ctl.no_binary:
1242+
result.discard('binary')
1243+
return frozenset(result)
1244+
1245+
1246+
def fmt_ctl_no_use_wheel(fmt_ctl):
1247+
fmt_ctl_handle_mutual_exclude(
1248+
':all:', fmt_ctl.no_binary, fmt_ctl.only_binary)
1249+
warnings.warn(
1250+
'--no-use-wheel is deprecated and will be removed in the future. '
1251+
' Please use --no-binary :all: instead.')
1252+
1253+
11951254
Search = namedtuple('Search', 'supplied canonical formats')
11961255
"""Capture key aspects of a search.
11971256

0 commit comments

Comments
 (0)