From 6aec23cafebc564922beeca9a3491ae611daa079 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Fri, 17 Apr 2015 15:03:34 +1200 Subject: [PATCH] Issue #2675: Granular control over wheels/sdists With wheel autobuilding in place a release blocker is some granular way to opt-out of wheels for known-bad packages. This patch introduces two new options: --no-binary and --only-binary to control what archives we are willing to use on both a global and per-package basis. This also closes #2084 --- CHANGES.txt | 3 + docs/reference/pip_install.rst | 3 +- docs/user_guide.rst | 2 +- pip/cmdoptions.py | 57 ++++++++++++++- pip/commands/freeze.py | 4 +- pip/commands/install.py | 7 +- pip/commands/list.py | 11 +-- pip/commands/uninstall.py | 4 +- pip/commands/wheel.py | 7 +- pip/index.py | 83 ++++++++++++++++++---- pip/req/req_file.py | 24 ++++++- pip/req/req_install.py | 2 +- pip/wheel.py | 25 +++++-- tests/data/reqfiles/README.txt | 9 +++ tests/data/reqfiles/supported_options2.txt | 5 ++ tests/functional/test_install.py | 54 +++++++++++++- tests/functional/test_install_extras.py | 2 +- tests/functional/test_install_upgrade.py | 2 +- tests/functional/test_wheel.py | 9 +++ tests/unit/test_cmdoptions.py | 62 ++++++++++++++++ tests/unit/test_finder.py | 17 ++++- tests/unit/test_req_file.py | 15 +++- 22 files changed, 365 insertions(+), 42 deletions(-) create mode 100644 tests/data/reqfiles/README.txt create mode 100644 tests/data/reqfiles/supported_options2.txt create mode 100644 tests/unit/test_cmdoptions.py diff --git a/CHANGES.txt b/CHANGES.txt index 0c95af1d0..890910134 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -25,6 +25,9 @@ * Build Wheels prior to installing from sdist, caching them in the pip cache directory to speed up subsequent installs. (:pull:`2618`) +* Allow fine grained control over the use of wheels and source builds. + (:pull:`2699`) + **6.1.1 (2015-04-07)** * No longer ignore dependencies which have been added to the standard library, diff --git a/docs/reference/pip_install.rst b/docs/reference/pip_install.rst index e18dcc4b2..c2b5bee82 100644 --- a/docs/reference/pip_install.rst +++ b/docs/reference/pip_install.rst @@ -112,7 +112,8 @@ Additionally, the following Package Index Options are supported: * :ref:`--allow-external <--allow-external>` * :ref:`--allow-all-external <--allow-external>` * :ref:`--allow-unverified <--allow-unverified>` - * :ref:`--no-use-wheel ` + * :ref:`--no-binary ` + * :ref:`--only-binary ` For example, to specify :ref:`--no-index <--no-index>` and 2 :ref:`--find-links <--find-links>` locations: diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 7f22acfa9..25806a1a9 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -123,7 +123,7 @@ to building and installing from source archives. For more information, see the `PEP425 `_ Pip prefers Wheels where they are available. To disable this, use the -:ref:`--no-use-wheel ` flag for :ref:`pip install`. +:ref:`--no-binary ` flag for :ref:`pip install`. If no satisfactory wheels are found, pip will default to finding source archives. diff --git a/pip/cmdoptions.py b/pip/cmdoptions.py index 9eed409d3..f0a4c20f3 100644 --- a/pip/cmdoptions.py +++ b/pip/cmdoptions.py @@ -11,7 +11,9 @@ from __future__ import absolute_import from functools import partial from optparse import OptionGroup, SUPPRESS_HELP, Option -from pip.index import PyPI + +from pip.index import ( + PyPI, FormatControl, fmt_ctl_handle_mutual_exclude, fmt_ctl_no_use_wheel) from pip.locations import CA_BUNDLE_PATH, USER_CACHE_DIR, src_prefix @@ -27,6 +29,12 @@ def make_option_group(group, parser): return option_group +def resolve_wheel_no_use_binary(options): + if not options.use_wheel: + control = options.format_control + fmt_ctl_no_use_wheel(control) + + ########### # options # ########### @@ -339,6 +347,7 @@ src = partial( 'The default for global installs is "/src".' ) +# XXX: deprecated, remove in 9.0 use_wheel = partial( Option, '--use-wheel', @@ -354,9 +363,53 @@ no_use_wheel = partial( action='store_false', default=True, help=('Do not Find and prefer wheel archives when searching indexes and ' - 'find-links locations.'), + 'find-links locations. DEPRECATED in favour of --no-binary.'), ) + +def _get_format_control(values, option): + """Get a format_control object.""" + return getattr(values, option.dest) + + +def _handle_no_binary(option, opt_str, value, parser): + existing = getattr(parser.values, option.dest) + fmt_ctl_handle_mutual_exclude( + value, existing.no_binary, existing.only_binary) + + +def _handle_only_binary(option, opt_str, value, parser): + existing = getattr(parser.values, option.dest) + fmt_ctl_handle_mutual_exclude( + value, existing.only_binary, existing.no_binary) + + +def no_binary(): + return Option( + "--no-binary", dest="format_control", action="callback", + callback=_handle_no_binary, type="str", + default=FormatControl(set(), set()), + help="Do not use binary packages. Can be supplied multiple times, and " + "each time adds to the existing value. Accepts either :all: to " + "disable all binary packages, :none: to empty the set, or one or " + "more package names with commas between them. Note that some " + "packages are tricky to compile and may fail to install when " + "this option is used on them.") + + +def only_binary(): + return Option( + "--only-binary", dest="format_control", action="callback", + callback=_handle_only_binary, type="str", + default=FormatControl(set(), set()), + help="Do not use source packages. Can be supplied multiple times, and " + "each time adds to the existing value. Accepts either :all: to " + "disable all source packages, :none: to empty the set, or one or " + "more package names with commas between them. Packages without " + "binary distributions will fail to install when this option is " + "used on them.") + + cache_dir = partial( Option, "--cache-dir", diff --git a/pip/commands/freeze.py b/pip/commands/freeze.py index 41d4c5df7..ef4c624b9 100644 --- a/pip/commands/freeze.py +++ b/pip/commands/freeze.py @@ -2,6 +2,7 @@ from __future__ import absolute_import import sys +import pip from pip.basecommand import Command from pip.operations.freeze import freeze from pip.wheel import WheelCache @@ -55,7 +56,8 @@ class FreezeCommand(Command): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): - wheel_cache = WheelCache(options.cache_dir) + format_control = pip.index.FormatControl(set(), set()) + wheel_cache = WheelCache(options.cache_dir, format_control) freeze_kwargs = dict( requirement=options.requirement, find_links=options.find_links, diff --git a/pip/commands/install.py b/pip/commands/install.py index a1c084eeb..942ab0cff 100644 --- a/pip/commands/install.py +++ b/pip/commands/install.py @@ -153,6 +153,8 @@ class InstallCommand(RequirementCommand): cmd_opts.add_option(cmdoptions.use_wheel()) cmd_opts.add_option(cmdoptions.no_use_wheel()) + cmd_opts.add_option(cmdoptions.no_binary()) + cmd_opts.add_option(cmdoptions.only_binary()) cmd_opts.add_option( '--pre', @@ -179,8 +181,8 @@ class InstallCommand(RequirementCommand): """ return PackageFinder( find_links=options.find_links, + format_control=options.format_control, index_urls=index_urls, - use_wheel=options.use_wheel, allow_external=options.allow_external, allow_unverified=options.allow_unverified, allow_all_external=options.allow_all_external, @@ -191,6 +193,7 @@ class InstallCommand(RequirementCommand): ) def run(self, options, args): + cmdoptions.resolve_wheel_no_use_binary(options) if options.download_dir: options.ignore_installed = True @@ -239,7 +242,7 @@ class InstallCommand(RequirementCommand): finder = self._build_package_finder(options, index_urls, session) build_delete = (not (options.no_clean or options.build_dir)) - wheel_cache = WheelCache(options.cache_dir) + wheel_cache = WheelCache(options.cache_dir, options.format_control) with BuildDirectory(options.build_dir, delete=build_delete) as build_dir: requirement_set = RequirementSet( diff --git a/pip/commands/list.py b/pip/commands/list.py index 6ed40418a..c208999c7 100644 --- a/pip/commands/list.py +++ b/pip/commands/list.py @@ -6,7 +6,7 @@ from pip._vendor import pkg_resources from pip.basecommand import Command from pip.exceptions import DistributionNotFound -from pip.index import PackageFinder, Search +from pip.index import FormatControl, fmt_ctl_formats, PackageFinder, Search from pip.req import InstallRequirement from pip.utils import get_installed_distributions, dist_is_editable from pip.wheel import WheelCache @@ -131,7 +131,8 @@ class ListCommand(Command): user_only=options.user, include_editables=False, ) - wheel_cache = WheelCache(options.cache_dir) + format_control = FormatControl(set(), set()) + wheel_cache = WheelCache(options.cache_dir, format_control) for dist in installed_packages: req = InstallRequirement.from_line( dist.key, None, isolated=options.isolated_mode, @@ -148,10 +149,12 @@ class ListCommand(Command): except DistributionNotFound: continue else: + canonical_name = pkg_resources.safe_name(req.name).lower() + formats = fmt_ctl_formats(format_control, canonical_name) search = Search( req.name, - pkg_resources.safe_name(req.name).lower(), - ["source", "binary"]) + canonical_name, + formats) remote_version = finder._link_package_versions( link, search).version if link.is_wheel: diff --git a/pip/commands/uninstall.py b/pip/commands/uninstall.py index 489894ad8..8ba1a7c65 100644 --- a/pip/commands/uninstall.py +++ b/pip/commands/uninstall.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import pip from pip.wheel import WheelCache from pip.req import InstallRequirement, RequirementSet, parse_requirements from pip.basecommand import Command @@ -43,7 +44,8 @@ class UninstallCommand(Command): def run(self, options, args): with self._build_session(options) as session: - wheel_cache = WheelCache(options.cache_dir) + format_control = pip.index.FormatControl(set(), set()) + wheel_cache = WheelCache(options.cache_dir, format_control) requirement_set = RequirementSet( build_dir=None, src_dir=None, diff --git a/pip/commands/wheel.py b/pip/commands/wheel.py index 2fa05f06f..f2d9eddda 100644 --- a/pip/commands/wheel.py +++ b/pip/commands/wheel.py @@ -61,6 +61,8 @@ class WheelCommand(RequirementCommand): ) cmd_opts.add_option(cmdoptions.use_wheel()) cmd_opts.add_option(cmdoptions.no_use_wheel()) + cmd_opts.add_option(cmdoptions.no_binary()) + cmd_opts.add_option(cmdoptions.only_binary()) cmd_opts.add_option( '--build-option', dest='build_options', @@ -122,6 +124,7 @@ class WheelCommand(RequirementCommand): def run(self, options, args): self.check_required_packages() + cmdoptions.resolve_wheel_no_use_binary(options) index_urls = [options.index_url] + options.extra_index_urls if options.no_index: @@ -143,8 +146,8 @@ class WheelCommand(RequirementCommand): finder = PackageFinder( find_links=options.find_links, + format_control=options.format_control, index_urls=index_urls, - use_wheel=options.use_wheel, allow_external=options.allow_external, allow_unverified=options.allow_unverified, allow_all_external=options.allow_all_external, @@ -155,7 +158,7 @@ class WheelCommand(RequirementCommand): ) build_delete = (not (options.no_clean or options.build_dir)) - wheel_cache = WheelCache(options.cache_dir) + wheel_cache = WheelCache(options.cache_dir, options.format_control) with BuildDirectory(options.build_dir, delete=build_delete) as build_dir: requirement_set = RequirementSet( diff --git a/pip/index.py b/pip/index.py index 39a67bf1f..4d820528f 100644 --- a/pip/index.py +++ b/pip/index.py @@ -34,7 +34,7 @@ from pip._vendor.packaging.version import parse as parse_version from pip._vendor.requests.exceptions import SSLError -__all__ = ['PackageFinder'] +__all__ = ['FormatControl', 'fmt_ctl_handle_mutual_exclude', 'PackageFinder'] # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC) @@ -97,14 +97,20 @@ class PackageFinder(object): """This finds packages. This is meant to match easy_install's technique for looking for - packages, by reading pages and looking for appropriate links + packages, by reading pages and looking for appropriate links. """ def __init__(self, find_links, index_urls, - use_wheel=True, allow_external=(), allow_unverified=(), + allow_external=(), allow_unverified=(), allow_all_external=False, allow_all_prereleases=False, trusted_hosts=None, process_dependency_links=False, - session=None): + session=None, format_control=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. + """ if session is None: raise TypeError( "PackageFinder() missing 1 required keyword argument: " @@ -130,7 +136,7 @@ class PackageFinder(object): # These are boring links that have already been logged somehow: self.logged_links = set() - self.use_wheel = use_wheel + self.format_control = format_control or FormatControl(set(), set()) # Do we allow (safe and verifiable) externally hosted files? self.allow_external = set(normalize_name(n) for n in allow_external) @@ -413,13 +419,9 @@ class PackageFinder(object): for location in url_locations: logger.debug('* %s', location) - formats = set(["source"]) - if self.use_wheel: - formats.add("binary") - search = Search( - project_name.lower(), - pkg_resources.safe_name(project_name).lower(), - frozenset(formats)) + canonical_name = pkg_resources.safe_name(project_name).lower() + formats = fmt_ctl_formats(self.format_control, canonical_name) + search = Search(project_name.lower(), canonical_name, formats) find_links_versions = self._package_versions( # We trust every directly linked archive in find_links (Link(url, '-f', trusted=True) for url in self.find_links), @@ -686,6 +688,7 @@ class PackageFinder(object): version = None if link.egg_fragment: egg_info = link.egg_fragment + ext = link.ext else: egg_info, ext = link.splitext() if not ext: @@ -743,6 +746,12 @@ class PackageFinder(object): return version = wheel.version + # This should be up by the search.ok_binary check, but see issue 2700. + if "source" not in search.formats and ext != wheel_ext: + self._log_skipped_link( + link, 'No sources permitted for %s' % search.supplied) + return + if not version: version = egg_info_matches(egg_info, search.supplied, link) if version is None: @@ -1192,6 +1201,56 @@ class Link(object): INSTALLED_VERSION = Link(Inf) +FormatControl = namedtuple('FormatControl', 'no_binary only_binary') +"""This object has two fields, no_binary and only_binary. + +If a field is falsy, it isn't set. If it is {':all:'}, it should match all +packages except those listed in the other field. Only one field can be set +to {':all:'} at a time. The rest of the time exact package name matches +are listed, with any given package only showing up in one field at a time. +""" + + +def fmt_ctl_handle_mutual_exclude(value, target, other): + new = value.split(',') + while ':all:' in new: + other.clear() + target.clear() + target.add(':all:') + del new[:new.index(':all:') + 1] + if ':none:' not in new: + # Without a none, we want to discard everything as :all: covers it + return + for name in new: + if name == ':none:': + target.clear() + continue + name = pkg_resources.safe_name(name).lower() + other.discard(name) + target.add(name) + + +def fmt_ctl_formats(fmt_ctl, canonical_name): + result = set(["binary", "source"]) + if canonical_name in fmt_ctl.only_binary: + result.discard('source') + elif canonical_name in fmt_ctl.no_binary: + result.discard('binary') + elif ':all:' in fmt_ctl.only_binary: + result.discard('source') + elif ':all:' in fmt_ctl.no_binary: + result.discard('binary') + return frozenset(result) + + +def fmt_ctl_no_use_wheel(fmt_ctl): + fmt_ctl_handle_mutual_exclude( + ':all:', fmt_ctl.no_binary, fmt_ctl.only_binary) + warnings.warn( + '--no-use-wheel is deprecated and will be removed in the future. ' + ' Please use --no-binary :all: instead.') + + Search = namedtuple('Search', 'supplied canonical formats') """Capture key aspects of a search. diff --git a/pip/req/req_file.py b/pip/req/req_file.py index 3c519a14c..46e97c7b1 100644 --- a/pip/req/req_file.py +++ b/pip/req/req_file.py @@ -12,6 +12,7 @@ import optparse from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves import filterfalse +import pip from pip.download import get_file_content from pip.req.req_install import InstallRequirement from pip.exceptions import (RequirementsFileParseError, @@ -45,6 +46,8 @@ SUPPORTED_OPTIONS = [ SUPPORTED_OPTIONS_REQ = [ cmdoptions.install_options, cmdoptions.global_options, + cmdoptions.no_binary, + cmdoptions.only_binary, ] # the 'dest' string values @@ -91,12 +94,27 @@ def process_line(line, filename, line_number, finder=None, comes_from=None, Process a single requirements line; This can result in creating/yielding requirements, or updating the finder. """ - parser = build_parser() + values = parser.get_default_values() + if finder: + values.format_control = finder.format_control + else: + # Undo the hack that removes defaults so that + # this can be parsed correctly. + values.format_control = pip.index.FormatControl(set(), set()) + orig_no_binary = frozenset(values.format_control.no_binary) + orig_only_binary = frozenset(values.format_control.only_binary) args = shlex.split(line) - opts, args = parser.parse_args(args) - req = None + opts, args = parser.parse_args(args, values) + if opts.use_wheel is False and finder: + pip.index.fmt_ctl_no_use_wheel(finder.format_control) + setattr(values, 'use_wheel', None) + if (orig_no_binary == opts.format_control.no_binary and + orig_only_binary == opts.format_control.only_binary): + # Make the per-requirement-line check work. + setattr(values, 'format_control', None) + req = None if args: for key, value in opts.__dict__.items(): # only certain options can be on req lines diff --git a/pip/req/req_install.py b/pip/req/req_install.py index f58caf5d6..ddf083bdf 100644 --- a/pip/req/req_install.py +++ b/pip/req/req_install.py @@ -257,7 +257,7 @@ class InstallRequirement(object): if self._wheel_cache is None: self._link = link else: - self._link = self._wheel_cache.cached_wheel(link) + self._link = self._wheel_cache.cached_wheel(link, self.name) @property def specifier(self): diff --git a/pip/wheel.py b/pip/wheel.py index 90227a3a0..253d32171 100644 --- a/pip/wheel.py +++ b/pip/wheel.py @@ -47,15 +47,19 @@ logger = logging.getLogger(__name__) class WheelCache(object): """A cache of wheels for future installs.""" - def __init__(self, cache_dir): + def __init__(self, cache_dir, format_control): """Create a wheel cache. :param cache_dir: The root of the cache. + :param format_control: A pip.index.FormatControl object to limit + binaries being read from the cache. """ self._cache_dir = cache_dir + self._format_control = format_control - def cached_wheel(self, link): - return cached_wheel(self._cache_dir, link) + def cached_wheel(self, link, package_name): + return cached_wheel( + self._cache_dir, link, self._format_control, package_name) def _cache_for_filename(cache_dir, sdistfilename): @@ -78,13 +82,19 @@ def _cache_for_filename(cache_dir, sdistfilename): return os.path.join(cache_dir, 'wheels', sdistfilename) -def cached_wheel(cache_dir, link): +def cached_wheel(cache_dir, link, format_control, package_name): if not cache_dir: return link if not link: return link if link.is_wheel: return link + if not package_name: + return link + canonical_name = pkg_resources.safe_name(package_name).lower() + formats = pip.index.fmt_ctl_formats(format_control, canonical_name) + if "binary" not in formats: + return link root = _cache_for_filename(cache_dir, link.filename) try: wheel_names = os.listdir(root) @@ -693,6 +703,13 @@ class WheelBuilder(object): # Doesn't look like a package - don't autobuild a wheel # because we'll have no way to lookup the result sanely continue + if "binary" not in pip.index.fmt_ctl_formats( + self.finder.format_control, + pkg_resources.safe_name(req.name).lower()): + logger.info( + "Skipping bdist_wheel for %s, due to binaries " + "being disabled for it.", req.name) + continue buildset.append(req) if not buildset: diff --git a/tests/data/reqfiles/README.txt b/tests/data/reqfiles/README.txt new file mode 100644 index 000000000..be4f3830f --- /dev/null +++ b/tests/data/reqfiles/README.txt @@ -0,0 +1,9 @@ +supported_options.txt +--------------------- + +Contains --no-use-wheel. + +supported_options2.txt +---------------------- + +Contains --no-binary and --only-binary options. diff --git a/tests/data/reqfiles/supported_options2.txt b/tests/data/reqfiles/supported_options2.txt new file mode 100644 index 000000000..25fa4afdb --- /dev/null +++ b/tests/data/reqfiles/supported_options2.txt @@ -0,0 +1,5 @@ +# default is no constraints +# We're not testing the format control logic here, just that the options are +# accepted +--no-binary fred +--only-binary wilma diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 217038583..96542b7a6 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -22,7 +22,7 @@ def test_without_setuptools(script, data): "'install', " "'INITools==0.2', " "'-f', '%s', " - "'--no-use-wheel'])" % data.packages, + "'--no-binary=:all:'])" % data.packages, expect_error=True, ) assert ( @@ -605,7 +605,7 @@ def test_compiles_pyc(script): Test installing with --compile on """ del script.environ["PYTHONDONTWRITEBYTECODE"] - script.pip("install", "--compile", "--no-use-wheel", "INITools==0.2") + script.pip("install", "--compile", "--no-binary=:all:", "INITools==0.2") # There are many locations for the __init__.pyc file so attempt to find # any of them @@ -626,7 +626,7 @@ def test_no_compiles_pyc(script, data): Test installing from wheel with --compile on """ del script.environ["PYTHONDONTWRITEBYTECODE"] - script.pip("install", "--no-compile", "--no-use-wheel", "INITools==0.2") + script.pip("install", "--no-compile", "--no-binary=:all:", "INITools==0.2") # There are many locations for the __init__.pyc file so attempt to find # any of them @@ -714,3 +714,51 @@ def test_install_builds_wheels(script, data): assert "Running setup.py install for requires-wheel" in str(res), str(res) # wheelbroken has to run install assert "Running setup.py install for wheelb" in str(res), str(res) + + +def test_install_no_binary_disables_building_wheels(script, data): + script.pip('install', 'wheel') + to_install = data.packages.join('requires_wheelbroken_upper') + res = script.pip( + 'install', '--no-index', '--no-binary=upper', '-f', data.find_links, + to_install, expect_stderr=True) + expected = ("Successfully installed requires-wheelbroken-upper-0" + " upper-2.0 wheelbroken-0.1") + # Must have installed it all + assert expected in str(res), str(res) + root = appdirs.user_cache_dir('pip') + wheels = [] + for top, dirs, files in os.walk(root): + wheels.extend(files) + # and built wheels for wheelbroken only + assert "Running setup.py bdist_wheel for wheelb" in str(res), str(res) + # But not requires_wheel... which is a local dir and thus uncachable. + assert "Running setup.py bdist_wheel for requir" not in str(res), str(res) + # Nor upper, which was blacklisted + assert "Running setup.py bdist_wheel for upper" not in str(res), str(res) + # wheelbroken has to run install + # into the cache + assert wheels != [], str(res) + # the local tree can't build a wheel (because we can't assume that every + # build will have a suitable unique key to cache on). + assert "Running setup.py install for requires-wheel" in str(res), str(res) + # And these two fell back to sdist based installed. + assert "Running setup.py install for wheelb" in str(res), str(res) + assert "Running setup.py install for upper" in str(res), str(res) + + +def test_install_no_binary_disables_cached_wheels(script, data): + script.pip('install', 'wheel') + # Seed the cache + script.pip( + 'install', '--no-index', '-f', data.find_links, + 'upper') + script.pip('uninstall', 'upper', '-y') + res = script.pip( + 'install', '--no-index', '--no-binary=:all:', '-f', data.find_links, + 'upper', expect_stderr=True) + assert "Successfully installed upper-2.0" in str(res), str(res) + # No wheel building for upper, which was blacklisted + assert "Running setup.py bdist_wheel for upper" not in str(res), str(res) + # Must have used source, not a cached wheel to install upper. + assert "Running setup.py install for upper" in str(res), str(res) diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index 1173d69b7..28759a27c 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -61,7 +61,7 @@ def test_nonexistent_extra_warns_user_no_wheel(script, data): This exercises source installs. """ result = script.pip( - 'install', '--no-use-wheel', '--no-index', + 'install', '--no-binary=:all:', '--no-index', '--find-links=' + data.find_links, 'simple[nonexistent]', expect_stderr=True, ) diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index d13461c93..8e253341c 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -333,7 +333,7 @@ class TestUpgradeSetuptools(object): self, script, data, virtualenv): self.prep_ve(script, '1.9.1', virtualenv.pip_source_dir) result = self.script.run( - self.ve_bin / 'pip', 'install', '--no-use-wheel', '--no-index', + self.ve_bin / 'pip', 'install', '--no-binary=:all:', '--no-index', '--find-links=%s' % data.find_links, '-U', 'setuptools' ) assert ( diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index a38974905..949c27535 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -50,6 +50,15 @@ def test_pip_wheel_downloads_wheels(script, data): assert "Saved" in result.stdout, result.stdout +@pytest.mark.network +def test_pip_wheel_builds_when_no_binary_set(script, data): + script.pip('install', 'wheel') + res = script.pip( + 'wheel', '--no-index', '--no-binary', ':all:', '-f', data.find_links, + 'setuptools==0.9.8') + assert "Running setup.py bdist_wheel for setuptools" in str(res), str(res) + + @pytest.mark.network def test_pip_wheel_builds_editable_deps(script, data): """ diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py new file mode 100644 index 000000000..5440a9a51 --- /dev/null +++ b/tests/unit/test_cmdoptions.py @@ -0,0 +1,62 @@ +import pip +from pip.basecommand import Command +from pip import cmdoptions + + +class SimpleCommand(Command): + name = 'fake' + summary = name + + def __init__(self): + super(SimpleCommand, self).__init__() + self.cmd_opts.add_option(cmdoptions.no_use_wheel()) + self.cmd_opts.add_option(cmdoptions.no_binary()) + self.cmd_opts.add_option(cmdoptions.only_binary()) + + def run(self, options, args): + cmdoptions.resolve_wheel_no_use_binary(options) + self.options = options + + +def test_no_use_wheel_sets_no_binary_all(): + cmd = SimpleCommand() + cmd.main(['fake', '--no-use-wheel']) + expected = pip.index.FormatControl(set([':all:']), set([])) + assert cmd.options.format_control == expected + + +def test_no_binary_overrides(): + cmd = SimpleCommand() + cmd.main(['fake', '--only-binary=:all:', '--no-binary=fred']) + expected = pip.index.FormatControl(set(['fred']), set([':all:'])) + assert cmd.options.format_control == expected + + +def test_only_binary_overrides(): + cmd = SimpleCommand() + cmd.main(['fake', '--no-binary=:all:', '--only-binary=fred']) + expected = pip.index.FormatControl(set([':all:']), set(['fred'])) + assert cmd.options.format_control == expected + + +def test_none_resets(): + cmd = SimpleCommand() + cmd.main(['fake', '--no-binary=:all:', '--no-binary=:none:']) + expected = pip.index.FormatControl(set([]), set([])) + assert cmd.options.format_control == expected + + +def test_none_preserves_other_side(): + cmd = SimpleCommand() + cmd.main( + ['fake', '--no-binary=:all:', '--only-binary=fred', + '--no-binary=:none:']) + expected = pip.index.FormatControl(set([]), set(['fred'])) + assert cmd.options.format_control == expected + + +def test_comma_separated_values(): + cmd = SimpleCommand() + cmd.main(['fake', '--no-binary=1,2,3']) + expected = pip.index.FormatControl(set(['1', '2', '3']), set([])) + assert cmd.options.format_control == expected diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 10b3afeaa..ffb6e3dcf 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -5,7 +5,9 @@ import pip.pep425tags from pkg_resources import parse_version, Distribution from pip.req import InstallRequirement -from pip.index import InstallationCandidate, PackageFinder, Link +from pip.index import ( + InstallationCandidate, PackageFinder, Link, FormatControl, + fmt_ctl_formats) from pip.exceptions import ( BestVersionAlreadyInstalled, DistributionNotFound, InstallationError, ) @@ -760,3 +762,16 @@ def test_find_all_versions_find_links_and_index(data): versions = finder._find_all_versions('simple') # first the find-links versions then the page versions assert [str(v.version) for v in versions] == ['3.0', '2.0', '1.0', '1.0'] + + +def test_fmt_ctl_matches(): + fmt = FormatControl(set(), set()) + assert fmt_ctl_formats(fmt, "fred") == frozenset(["source", "binary"]) + fmt = FormatControl(set(["fred"]), set()) + assert fmt_ctl_formats(fmt, "fred") == frozenset(["source"]) + fmt = FormatControl(set(["fred"]), set([":all:"])) + assert fmt_ctl_formats(fmt, "fred") == frozenset(["source"]) + fmt = FormatControl(set(), set(["fred"])) + assert fmt_ctl_formats(fmt, "fred") == frozenset(["binary"]) + fmt = FormatControl(set([":all:"]), set(["fred"])) + assert fmt_ctl_formats(fmt, "fred") == frozenset(["binary"]) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 72073fe08..a0df0b029 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -6,6 +6,7 @@ from mock import patch import pytest from pretend import stub +import pip from pip.exceptions import (RequirementsFileParseError, ReqFileOnleOneOptionPerLineError, ReqFileOptionNotAllowedWithReqError) @@ -175,11 +176,13 @@ class TestProcessLine(object): def test_set_finder_use_wheel(self, finder): list(process_line("--use-wheel", "file", 1, finder=finder)) - assert finder.use_wheel is True + no_use_wheel_fmt = pip.index.FormatControl(set(), set()) + assert finder.format_control == no_use_wheel_fmt def test_set_finder_no_use_wheel(self, finder): list(process_line("--no-use-wheel", "file", 1, finder=finder)) - assert finder.use_wheel is False + no_use_wheel_fmt = pip.index.FormatControl(set([':all:']), set()) + assert finder.format_control == no_use_wheel_fmt def test_noop_always_unzip(self, finder): # noop, but confirm it can be set @@ -262,6 +265,14 @@ class TestParseRequirements(object): assert finder.index_urls == ['url1', 'url2'] + def test_req_file_parse_no_only_binary(self, data): + finder = PackageFinder([], [], session=PipSession()) + list(parse_requirements( + data.reqfiles.join("supported_options2.txt"), finder, + session=PipSession())) + expected = pip.index.FormatControl(set(['fred']), set(['wilma'])) + assert finder.format_control == expected + def test_req_file_parse_comment_start_of_line(self, tmpdir, finder): """ Test parsing comments in a requirements file