diff --git a/pip/req/req_set.py b/pip/req/req_set.py index 2feb25e4c71..74a86752979 100644 --- a/pip/req/req_set.py +++ b/pip/req/req_set.py @@ -241,7 +241,8 @@ def add_requirement(self, install_req, parent_req_name=None): except KeyError: existing_req = None if (parent_req_name is None and existing_req and not - existing_req.constraint): + existing_req.constraint and + existing_req.extras == install_req.extras): raise InstallationError( 'Double requirement given: %s (already in %s, name=%r)' % (install_req, existing_req, name)) @@ -267,6 +268,11 @@ def add_requirement(self, install_req, parent_req_name=None): # If we're now installing a constraint, mark the existing # object for real installation. existing_req.constraint = False + existing_req.extras = tuple( + sorted(set(existing_req.extras).union( + set(install_req.extras)))) + logger.debug("Setting %s extras to: %s" % ( + existing_req, existing_req.extras)) # And now we need to scan this. result = [existing_req] # Canonicalise to the already-added object for the backref diff --git a/tests/data/packages/LocalExtras-0.0.2/.gitignore b/tests/data/packages/LocalExtras-0.0.2/.gitignore new file mode 100644 index 00000000000..bc186a3f1cf --- /dev/null +++ b/tests/data/packages/LocalExtras-0.0.2/.gitignore @@ -0,0 +1 @@ +/LocalExtras-0.0.2.egg-info diff --git a/tests/data/packages/LocalExtras-0.0.2/localextras/__init__.py b/tests/data/packages/LocalExtras-0.0.2/localextras/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/packages/LocalExtras-0.0.2/setup.py b/tests/data/packages/LocalExtras-0.0.2/setup.py new file mode 100644 index 00000000000..a32e344f5f0 --- /dev/null +++ b/tests/data/packages/LocalExtras-0.0.2/setup.py @@ -0,0 +1,30 @@ +import os +from setuptools import setup, find_packages + + +def path_to_url(path): + """ + Convert a path to URI. The path will be made absolute and + will not have quoted path parts. + """ + path = os.path.normpath(os.path.abspath(path)) + drive, path = os.path.splitdrive(path) + filepath = path.split(os.path.sep) + url = '/'.join(filepath) + if drive: + return 'file:///' + drive + url + return 'file://' +url + + +HERE = os.path.dirname(__file__) +DEP_PATH = os.path.join(HERE, '..', '..', 'indexes', 'simple', 'simple') +DEP_URL = path_to_url(DEP_PATH) + +setup( + name='LocalExtras', + version='0.0.2', + packages=find_packages(), + install_requires=['simple==1.0'], + extras_require={ 'bar': ['simple==2.0'], 'baz': ['singlemodule'] }, + dependency_links=[DEP_URL] +) diff --git a/tests/data/packages/LocalExtras/setup.py b/tests/data/packages/LocalExtras/setup.py index 7c2839819a8..fea6e16039a 100644 --- a/tests/data/packages/LocalExtras/setup.py +++ b/tests/data/packages/LocalExtras/setup.py @@ -24,6 +24,6 @@ def path_to_url(path): name='LocalExtras', version='0.0.1', packages=find_packages(), - extras_require={ 'bar': ['simple'] }, + extras_require={ 'bar': ['simple'], 'baz': ['singlemodule'] }, dependency_links=[DEP_URL] ) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 5c2ed0c6493..f30d0852655 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -343,3 +343,95 @@ def test_double_install_spurious_hash_mismatch(script, tmpdir): result = script.pip_install_local( '-r', reqs_file.abspath, expect_error=False) assert 'Successfully installed simple-1.0' in str(result) + + +def test_install_with_extras_from_constraints(script, data): + to_install = data.packages.join("LocalExtras") + script.scratch_path.join("constraints.txt").write( + "file://%s#egg=LocalExtras[bar]" % to_install + ) + result = script.pip_install_local( + '-c', script.scratch_path / 'constraints.txt', 'LocalExtras') + assert script.site_packages / 'simple' in result.files_created + + +def test_install_with_extras_from_install(script, data): + to_install = data.packages.join("LocalExtras") + script.scratch_path.join("constraints.txt").write( + "file://%s#egg=LocalExtras" % to_install + ) + result = script.pip_install_local( + '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') + assert script.site_packages / 'singlemodule.py'in result.files_created + + +def test_install_with_extras_joined(script, data): + to_install = data.packages.join("LocalExtras") + script.scratch_path.join("constraints.txt").write( + "file://%s#egg=LocalExtras[bar]" % to_install + ) + result = script.pip_install_local( + '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]' + ) + assert script.site_packages / 'simple' in result.files_created + assert script.site_packages / 'singlemodule.py'in result.files_created + + +def test_install_with_extras_editable_joined(script, data): + to_install = data.packages.join("LocalExtras") + script.scratch_path.join("constraints.txt").write( + "-e file://%s#egg=LocalExtras[bar]" % to_install + ) + result = script.pip_install_local( + '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') + assert script.site_packages / 'simple' in result.files_created + assert script.site_packages / 'singlemodule.py'in result.files_created + + +def test_install_distribution_full_union(script, data): + to_install = data.packages.join("LocalExtras") + result = script.pip_install_local( + to_install, to_install + "[bar]", to_install + "[baz]") + assert 'Running setup.py install for LocalExtras' in result.stdout + assert script.site_packages / 'simple' in result.files_created + assert script.site_packages / 'singlemodule.py' in result.files_created + + +def test_install_distribution_duplicate_extras(script, data): + to_install = data.packages.join("LocalExtras") + package_name = to_install + "[bar]" + with pytest.raises(AssertionError): + result = script.pip_install_local(package_name, package_name) + assert 'Double requirement given: %s' % package_name in result.stderr + + +def test_install_distribution_union_with_constraints(script, data): + to_install = data.packages.join("LocalExtras") + script.scratch_path.join("constraints.txt").write( + "%s[bar]" % to_install) + result = script.pip_install_local( + '-c', script.scratch_path / 'constraints.txt', to_install + '[baz]') + assert 'Running setup.py install for LocalExtras' in result.stdout + assert script.site_packages / 'singlemodule.py' in result.files_created + + +def test_install_distribution_union_with_versions(script, data): + to_install_001 = data.packages.join("LocalExtras") + to_install_002 = data.packages.join("LocalExtras-0.0.2") + result = script.pip_install_local( + to_install_001 + "[bar]", to_install_002 + "[baz]") + assert ("Successfully installed LocalExtras-0.0.1 simple-3.0 " + + "singlemodule-0.0.1" in result.stdout) + + +@pytest.mark.xfail +def test_install_distribution_union_conflicting_extras(script, data): + # LocalExtras requires simple==1.0, LocalExtras[bar] requires simple==2.0; + # without a resolver, pip does not detect the conflict between simple==1.0 + # and simple==2.0. Once a resolver is added, this conflict should be + # detected. + to_install = data.packages.join("LocalExtras-0.0.2") + result = script.pip_install_local(to_install, to_install + "[bar]", + expect_error=True) + assert 'installed' not in result.stdout + assert "Conflict" in result.stderr