diff --git a/CHANGES.txt b/CHANGES.txt index d9d04e36ca7..84df59c3eed 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -31,6 +31,9 @@ * Fixed :issue:`1769`. `pip wheel` now returns an error code if any wheels fail to build. +* Fixed :issue:`1775`. `pip wheel` wasn't building wheels for dependencies of + editable requirements. + **1.5.5 (2014-05-03)** diff --git a/pip/cmdoptions.py b/pip/cmdoptions.py index 93fa62dbdc6..08b6069f44c 100644 --- a/pip/cmdoptions.py +++ b/pip/cmdoptions.py @@ -9,7 +9,7 @@ """ import copy from optparse import OptionGroup, SUPPRESS_HELP, Option -from pip.locations import build_prefix, default_log_file +from pip.locations import build_prefix, default_log_file, src_prefix def make_option_group(group, parser): @@ -286,6 +286,26 @@ def make(self): help='Install from the given requirements file. ' 'This option can be used multiple times.') +editable = OptionMaker( + '-e', '--editable', + dest='editables', + action='append', + default=[], + metavar='path/url', + help=('Install a project in editable mode (i.e. setuptools ' + '"develop mode") from a local project path or a VCS url.'), +) + +src = OptionMaker( + '--src', '--source', '--source-dir', '--source-directory', + dest='src_dir', + metavar='dir', + default=src_prefix, + help='Directory to check out editable projects into. ' + 'The default in a virtualenv is "/src". ' + 'The default for global installs is "/src".' +) + use_wheel = OptionMaker( '--use-wheel', dest='use_wheel', diff --git a/pip/commands/install.py b/pip/commands/install.py index 850a6ff6768..55690990f96 100644 --- a/pip/commands/install.py +++ b/pip/commands/install.py @@ -4,7 +4,7 @@ from pip.req import InstallRequirement, RequirementSet, parse_requirements from pip.log import logger -from pip.locations import (src_prefix, virtualenv_no_global, distutils_scheme, +from pip.locations import (virtualenv_no_global, distutils_scheme, build_prefix) from pip.basecommand import Command from pip.index import PackageFinder @@ -43,16 +43,7 @@ def __init__(self, *args, **kw): cmd_opts = self.cmd_opts - cmd_opts.add_option( - '-e', '--editable', - dest='editables', - action='append', - default=[], - metavar='path/url', - help=('Install a project in editable mode (i.e. setuptools ' - '"develop mode") from a local project path or a VCS url.'), - ) - + cmd_opts.add_option(cmdoptions.editable.make()) cmd_opts.add_option(cmdoptions.requirements.make()) cmd_opts.add_option(cmdoptions.build_dir.make()) @@ -73,15 +64,7 @@ def __init__(self, *args, **kw): ) cmd_opts.add_option(cmdoptions.download_cache.make()) - - cmd_opts.add_option( - '--src', '--source', '--source-dir', '--source-directory', - dest='src_dir', - metavar='dir', - default=src_prefix, - help='Directory to check out editable projects into. ' - 'The default in a virtualenv is "/src". ' - 'The default for global installs is "/src".') + cmd_opts.add_option(cmdoptions.src.make()) cmd_opts.add_option( '-U', '--upgrade', diff --git a/pip/commands/wheel.py b/pip/commands/wheel.py index ee755423864..c86d2af21b7 100644 --- a/pip/commands/wheel.py +++ b/pip/commands/wheel.py @@ -34,8 +34,8 @@ class WheelCommand(Command): usage = """ %prog [options] ... %prog [options] -r ... - %prog [options] ... - %prog [options] ... + %prog [options] [-e] ... + %prog [options] [-e] ... %prog [options] ...""" summary = 'Build wheels from your requirements.' @@ -61,8 +61,10 @@ def __init__(self, *args, **kw): metavar='options', action='append', help="Extra arguments to be supplied to 'setup.py bdist_wheel'.") + cmd_opts.add_option(cmdoptions.editable.make()) cmd_opts.add_option(cmdoptions.requirements.make()) cmd_opts.add_option(cmdoptions.download_cache.make()) + cmd_opts.add_option(cmdoptions.src.make()) cmd_opts.add_option(cmdoptions.no_deps.make()) cmd_opts.add_option(cmdoptions.build_dir.make()) @@ -158,7 +160,7 @@ def run(self, options, args): options.build_dir = os.path.abspath(options.build_dir) requirement_set = RequirementSet( build_dir=options.build_dir, - src_dir=None, + src_dir=options.src_dir, download_dir=None, download_cache=options.download_cache, ignore_dependencies=options.ignore_dependencies, @@ -175,16 +177,19 @@ def run(self, options, args): for name in args: requirement_set.add_requirement( InstallRequirement.from_line(name, None)) - + for name in options.editables: + requirement_set.add_requirement( + InstallRequirement.from_editable( + name, + default_vcs=options.default_vcs + ) + ) for filename in options.requirements: for req in parse_requirements( filename, finder=finder, options=options, session=session): - if req.editable: - logger.notify("ignoring %s" % req.url) - continue requirement_set.add_requirement(req) # fail if no requirements diff --git a/pip/wheel.py b/pip/wheel.py index 81513c283ac..e4853950ee0 100644 --- a/pip/wheel.py +++ b/pip/wheel.py @@ -546,7 +546,16 @@ def build(self): reqset = self.requirement_set.requirements.values() - buildset = [req for req in reqset if not req.is_wheel] + buildset = [] + for req in reqset: + if req.is_wheel: + logger.notify( + 'Skipping %s, due to already being wheel.' % req.name) + elif req.editable: + logger.notify( + 'Skipping %s, due to being editable' % req.name) + else: + buildset.append(req) if not buildset: return True diff --git a/tests/data/src/requires_simple/requires_simple/__init__.py b/tests/data/src/requires_simple/requires_simple/__init__.py new file mode 100644 index 00000000000..792d6005489 --- /dev/null +++ b/tests/data/src/requires_simple/requires_simple/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/data/src/requires_simple/setup.py b/tests/data/src/requires_simple/setup.py new file mode 100644 index 00000000000..3f7983b82c7 --- /dev/null +++ b/tests/data/src/requires_simple/setup.py @@ -0,0 +1,6 @@ +from setuptools import setup, find_packages + +setup(name='requires_simple', + version='0.1', + install_requires=['simple==1.0'] + ) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index d6f51642e54..039fd50b1e6 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -1,14 +1,11 @@ """'pip wheel' tests""" import os -import sys -import textwrap from os.path import exists -from pip.download import path_to_url as path_to_url_d from pip.locations import write_delete_marker_file from pip.status_codes import PREVIOUS_BUILD_DIR_ERROR -from tests.lib import pyversion, path_to_url +from tests.lib import pyversion def test_pip_wheel_fails_without_wheel(script, data): @@ -50,6 +47,20 @@ def test_pip_wheel_downloads_wheels(script, data): assert "Saved" in result.stdout, result.stdout +def test_pip_wheel_builds_editable_deps(script, data): + """ + Test 'pip wheel' finds and builds dependencies of editables + """ + script.pip('install', 'wheel') + editable_path = os.path.join(data.src, 'requires_simple') + result = script.pip( + 'wheel', '--no-index', '-f', data.find_links, '-e', editable_path + ) + wheel_file_name = 'simple-1.0-py%s-none-any.whl' % pyversion[0] + wheel_file_path = script.scratch / 'wheelhouse' / wheel_file_name + assert wheel_file_path in result.files_created, result.stdout + + def test_pip_wheel_fail(script, data): """ Test 'pip wheel' failure. @@ -70,38 +81,6 @@ def test_pip_wheel_fail(script, data): assert result.returncode != 0 -def test_pip_wheel_ignore_wheels_editables(script, data): - """ - Test 'pip wheel' ignores editables - """ - script.pip('install', 'wheel') - - local_wheel = '%s/simple.dist-0.1-py2.py3-none-any.whl' % data.find_links - local_editable = data.packages.join("FSPkg") - script.scratch_path.join("reqs.txt").write(textwrap.dedent("""\ - %s - -e %s - simple - """ % (local_wheel, local_editable))) - result = script.pip( - 'wheel', '--no-index', '-f', data.find_links, '-r', - script.scratch_path / 'reqs.txt', - ) - wheel_file_name = 'simple-3.0-py%s-none-any.whl' % pyversion[0] - wheel_file_path = script.scratch / 'wheelhouse' / wheel_file_name - assert wheel_file_path in result.files_created, ( - wheel_file_path, - result.files_created, - ) - assert "Successfully built simple" in result.stdout, result.stdout - assert "Failed to build" not in result.stdout, result.stdout - ignore_editable = "ignoring %s" % path_to_url(local_editable) - # TODO: understand this divergence - if sys.platform == 'win32': - ignore_editable = "ignoring %s" % path_to_url_d(local_editable) - assert ignore_editable in result.stdout, result.stdout - - def test_no_clean_option_blocks_cleaning_after_wheel(script, data): """ Test --no-clean option blocks cleaning after wheel build @@ -153,4 +132,4 @@ def test_pip_wheel_fail_cause_of_previous_build_dir(script, data): ) # Then I see that the error code is the right one - assert result.returncode == PREVIOUS_BUILD_DIR_ERROR + assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, result diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 69c265126b1..2d2e77ef2bc 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -70,6 +70,10 @@ def packages2(self): def packages3(self): return self.root.join("packages3") + @property + def src(self): + return self.root.join("src") + @property def indexes(self): return self.root.join("indexes") diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 7e838872458..5b813a91597 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -2,7 +2,7 @@ import os import pytest -from mock import patch +from mock import patch, Mock from pip._vendor import pkg_resources from pip import wheel @@ -283,3 +283,28 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): self.assert_installed() assert not os.path.isdir( os.path.join(self.dest_dist_info, 'empty_dir')) + + +class TestWheelBuilder(object): + + @patch('pip.log.Logger.log') + @patch('pip.wheel.WheelBuilder._build_one') + def test_skip_building_wheels(self, mock_build_one, mock_log): + wheel_req = Mock(is_wheel=True, editable=False) + reqset = Mock(requirements=Mock(values=lambda: [wheel_req])) + wb = wheel.WheelBuilder(reqset, Mock(), '/wheel/dir') + wb.build() + name, args, kwargs = mock_log.mock_calls[0] + assert "due to already being wheel" in args[1] + assert mock_build_one.mock_calls == [] + + @patch('pip.log.Logger.log') + @patch('pip.wheel.WheelBuilder._build_one') + def test_skip_building_editables(self, mock_build_one, mock_log): + editable_req = Mock(editable=True, is_wheel=False) + reqset = Mock(requirements=Mock(values=lambda: [editable_req])) + wb = wheel.WheelBuilder(reqset, Mock(), '/wheel/dir') + wb.build() + name, args, kwargs = mock_log.mock_calls[0] + assert "due to being editable" in args[1] + assert mock_build_one.mock_calls == []