diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst
index 61e0cdf0769..c36ac520528 100644
--- a/docs/html/user_guide.rst
+++ b/docs/html/user_guide.rst
@@ -254,9 +254,11 @@ Constraints Files
Constraints files are requirements files that only control which version of a
requirement is installed, not whether it is installed or not. Their syntax and
-contents is nearly identical to :ref:`Requirements Files`. There is one key
-difference: Including a package in a constraints file does not trigger
-installation of the package.
+contents is a subset of :ref:`Requirements Files`, with several kinds of syntax
+not allowed: constraints must have a name, they cannot be editable, and they
+cannot specify extras. In terms of semantics, there is one key difference:
+Including a package in a constraints file does not trigger installation of the
+package.
Use a constraints file like so:
diff --git a/news/8253.feature.rst b/news/8253.feature.rst
new file mode 100644
index 00000000000..196e4dd9613
--- /dev/null
+++ b/news/8253.feature.rst
@@ -0,0 +1 @@
+Add the ability for the new resolver to process URL constraints.
diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py
index 6f9a3244383..86d0be4079d 100644
--- a/src/pip/_internal/models/link.py
+++ b/src/pip/_internal/models/link.py
@@ -240,3 +240,9 @@ def is_hash_allowed(self, hashes):
assert self.hash is not None
return hashes.is_hash_allowed(self.hash_name, hex_digest=self.hash)
+
+
+# TODO: Relax this comparison logic to ignore, for example, fragments.
+def links_equivalent(link1, link2):
+ # type: (Link, Link) -> bool
+ return link1 == link2
diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py
index 810fc085988..b5e7e7c3e14 100644
--- a/src/pip/_internal/req/constructors.py
+++ b/src/pip/_internal/req/constructors.py
@@ -461,3 +461,19 @@ def install_req_from_parsed_requirement(
user_supplied=user_supplied,
)
return req
+
+
+def install_req_from_link_and_ireq(link, ireq):
+ # type: (Link, InstallRequirement) -> InstallRequirement
+ return InstallRequirement(
+ req=ireq.req,
+ comes_from=ireq.comes_from,
+ editable=ireq.editable,
+ link=link,
+ markers=ireq.markers,
+ use_pep517=ireq.use_pep517,
+ isolated=ireq.isolated,
+ install_options=ireq.install_options,
+ global_options=ireq.global_options,
+ hash_options=ireq.hash_options,
+ )
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 5eba2c2ae74..2c5434830d8 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -840,8 +840,8 @@ def check_invalid_constraint_type(req):
problem = ""
if not req.name:
problem = "Unnamed requirements are not allowed as constraints"
- elif req.link:
- problem = "Links are not allowed as constraints"
+ elif req.editable:
+ problem = "Editable requirements are not allowed as constraints"
elif req.extras:
problem = "Constraints cannot have extras"
diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py
index 81fee9b9e3e..d42bca8bfa4 100644
--- a/src/pip/_internal/resolution/resolvelib/base.py
+++ b/src/pip/_internal/resolution/resolvelib/base.py
@@ -4,7 +4,7 @@
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import _BaseVersion
-from pip._internal.models.link import Link
+from pip._internal.models.link import Link, links_equivalent
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.hashes import Hashes
@@ -20,24 +20,26 @@ def format_name(project, extras):
class Constraint:
- def __init__(self, specifier, hashes):
- # type: (SpecifierSet, Hashes) -> None
+ def __init__(self, specifier, hashes, links):
+ # type: (SpecifierSet, Hashes, FrozenSet[Link]) -> None
self.specifier = specifier
self.hashes = hashes
+ self.links = links
@classmethod
def empty(cls):
# type: () -> Constraint
- return Constraint(SpecifierSet(), Hashes())
+ return Constraint(SpecifierSet(), Hashes(), frozenset())
@classmethod
def from_ireq(cls, ireq):
# type: (InstallRequirement) -> Constraint
- return Constraint(ireq.specifier, ireq.hashes(trust_internet=False))
+ links = frozenset([ireq.link]) if ireq.link else frozenset()
+ return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links)
def __nonzero__(self):
# type: () -> bool
- return bool(self.specifier) or bool(self.hashes)
+ return bool(self.specifier) or bool(self.hashes) or bool(self.links)
def __bool__(self):
# type: () -> bool
@@ -49,10 +51,16 @@ def __and__(self, other):
return NotImplemented
specifier = self.specifier & other.specifier
hashes = self.hashes & other.hashes(trust_internet=False)
- return Constraint(specifier, hashes)
+ links = self.links
+ if other.link:
+ links = links.union([other.link])
+ return Constraint(specifier, hashes, links)
def is_satisfied_by(self, candidate):
# type: (Candidate) -> bool
+ # Reject if there are any mismatched URL constraints on this package.
+ if self.links and not all(_match_link(link, candidate) for link in self.links):
+ return False
# We can safely always allow prereleases here since PackageFinder
# already implements the prerelease logic, and would have filtered out
# prerelease candidates if the user does not expect them.
@@ -94,6 +102,13 @@ def format_for_error(self):
raise NotImplementedError("Subclass should override")
+def _match_link(link, candidate):
+ # type: (Link, Candidate) -> bool
+ if candidate.source_link:
+ return links_equivalent(link, candidate.source_link)
+ return False
+
+
class Candidate:
@property
def project_name(self):
diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py
index 035e118d022..ccd1129dfc9 100644
--- a/src/pip/_internal/resolution/resolvelib/candidates.py
+++ b/src/pip/_internal/resolution/resolvelib/candidates.py
@@ -8,7 +8,7 @@
from pip._vendor.pkg_resources import Distribution
from pip._internal.exceptions import HashError, MetadataInconsistent
-from pip._internal.models.link import Link
+from pip._internal.models.link import Link, links_equivalent
from pip._internal.models.wheel import Wheel
from pip._internal.req.constructors import (
install_req_from_editable,
@@ -155,7 +155,7 @@ def __hash__(self):
def __eq__(self, other):
# type: (Any) -> bool
if isinstance(other, self.__class__):
- return self._link == other._link
+ return links_equivalent(self._link, other._link)
return False
@property
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index 3181d575336..f740734061d 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -32,6 +32,7 @@
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
from pip._internal.operations.prepare import RequirementPreparer
+from pip._internal.req.constructors import install_req_from_link_and_ireq
from pip._internal.req.req_install import InstallRequirement
from pip._internal.resolution.base import InstallRequirementProvider
from pip._internal.utils.compatibility_tags import get_supported
@@ -264,6 +265,46 @@ def find_candidates(
if ireq is not None:
ireqs.append(ireq)
+ for link in constraint.links:
+ if not ireqs:
+ # If we hit this condition, then we cannot construct a candidate.
+ # However, if we hit this condition, then none of the requirements
+ # provided an ireq, so they must have provided an explicit candidate.
+ # In that case, either the candidate matches, in which case this loop
+ # doesn't need to do anything, or it doesn't, in which case there's
+ # nothing this loop can do to recover.
+ break
+ if link.is_wheel:
+ wheel = Wheel(link.filename)
+ # Check whether the provided wheel is compatible with the target
+ # platform.
+ if not wheel.supported(self._finder.target_python.get_tags()):
+ # We are constrained to install a wheel that is incompatible with
+ # the target architecture, so there are no valid candidates.
+ # Return early, with no candidates.
+ return ()
+ # Create a "fake" InstallRequirement that's basically a clone of
+ # what "should" be the template, but with original_link set to link.
+ # Using the given requirement is necessary for preserving hash
+ # requirements, but without the original_link, direct_url.json
+ # won't be created.
+ ireq = install_req_from_link_and_ireq(link, ireqs[0])
+ candidate = self._make_candidate_from_link(
+ link,
+ extras=frozenset(),
+ template=ireq,
+ name=canonicalize_name(ireq.name) if ireq.name else None,
+ version=None,
+ )
+ if candidate is None:
+ # _make_candidate_from_link returns None if the wheel fails to build.
+ # We are constrained to install this wheel, so there are no valid
+ # candidates.
+ # Return early, with no candidates.
+ return ()
+
+ explicit_candidates.add(candidate)
+
# If none of the requirements want an explicit candidate, we can ask
# the finder for candidates.
if not explicit_candidates:
diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py
index 23273774d16..e28a7e9b57e 100644
--- a/tests/functional/test_install_direct_url.py
+++ b/tests/functional/test_install_direct_url.py
@@ -1,5 +1,7 @@
import re
+import pytest
+
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
from tests.lib import _create_test_package, path_to_url
@@ -46,3 +48,24 @@ def test_install_archive_direct_url(script, data, with_wheel):
assert req.startswith("simple @ file://")
result = script.pip("install", req)
assert _get_created_direct_url(result, "simple")
+
+
+@pytest.mark.network
+def test_install_vcs_constraint_direct_url(script, with_wheel):
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text(
+ "git+https://github.com/pypa/pip-test-package"
+ "@5547fa909e83df8bd743d3978d6667497983a4b7"
+ "#egg=pip-test-package"
+ )
+ result = script.pip("install", "pip-test-package", "-c", constraints_file)
+ assert _get_created_direct_url(result, "pip_test_package")
+
+
+def test_install_vcs_constraint_direct_file_url(script, with_wheel):
+ pkg_path = _create_test_package(script, name="testpkg")
+ url = path_to_url(pkg_path)
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text(f"git+{url}#egg=testpkg")
+ result = script.pip("install", "testpkg", "-c", constraints_file)
+ assert _get_created_direct_url(result, "testpkg")
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
index d559e94be18..d8de6248b7d 100644
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -405,7 +405,7 @@ def test_constraints_constrain_to_local_editable(
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- assert 'Links are not allowed as constraints' in result.stderr
+ assert 'Editable requirements are not allowed as constraints' in result.stderr
else:
assert 'Running setup.py develop for singlemodule' in result.stdout
@@ -419,12 +419,8 @@ def test_constraints_constrain_to_local(script, data, resolver_variant):
'install', '--no-index', '-f', data.find_links, '-c',
script.scratch_path / 'constraints.txt', 'singlemodule',
allow_stderr_warning=True,
- expect_error=(resolver_variant == "2020-resolver"),
)
- if resolver_variant == "2020-resolver":
- assert 'Links are not allowed as constraints' in result.stderr
- else:
- assert 'Running setup.py install for singlemodule' in result.stdout
+ assert 'Running setup.py install for singlemodule' in result.stdout
def test_constrained_to_url_install_same_url(script, data, resolver_variant):
@@ -438,7 +434,11 @@ def test_constrained_to_url_install_same_url(script, data, resolver_variant):
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- assert 'Links are not allowed as constraints' in result.stderr
+ assert 'Cannot install singlemodule 0.0.1' in result.stderr, str(result)
+ assert (
+ 'because these package versions have conflicting dependencies.'
+ in result.stderr
+ ), str(result)
else:
assert ('Running setup.py install for singlemodule'
in result.stdout), str(result)
@@ -489,7 +489,7 @@ def test_install_with_extras_from_constraints(script, data, resolver_variant):
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- assert 'Links are not allowed as constraints' in result.stderr
+ assert 'Constraints cannot have extras' in result.stderr
else:
result.did_create(script.site_packages / 'simple')
@@ -521,7 +521,7 @@ def test_install_with_extras_joined(script, data, resolver_variant):
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- assert 'Links are not allowed as constraints' in result.stderr
+ assert 'Constraints cannot have extras' in result.stderr
else:
result.did_create(script.site_packages / 'simple')
result.did_create(script.site_packages / 'singlemodule.py')
@@ -538,7 +538,7 @@ def test_install_with_extras_editable_joined(script, data, resolver_variant):
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- assert 'Links are not allowed as constraints' in result.stderr
+ assert 'Editable requirements are not allowed as constraints' in result.stderr
else:
result.did_create(script.site_packages / 'simple')
result.did_create(script.site_packages / 'singlemodule.py')
diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py
index 16f9f4f4216..78a21a3c115 100644
--- a/tests/functional/test_new_resolver.py
+++ b/tests/functional/test_new_resolver.py
@@ -10,7 +10,9 @@
create_basic_sdist_for_package,
create_basic_wheel_for_package,
create_test_package_with_setup,
+ path_to_url,
)
+from tests.lib.path import Path
from tests.lib.wheel import make_wheel
@@ -45,6 +47,24 @@ def assert_editable(script, *args):
f"{args!r} not all found in {script.site_packages_path!r}"
+@pytest.fixture()
+def make_fake_wheel(script):
+
+ def _make_fake_wheel(name, version, wheel_tag):
+ wheel_house = script.scratch_path.joinpath("wheelhouse")
+ wheel_house.mkdir()
+ wheel_builder = make_wheel(
+ name=name,
+ version=version,
+ wheel_metadata_updates={"Tag": []},
+ )
+ wheel_path = wheel_house.joinpath(f"{name}-{version}-{wheel_tag}.whl")
+ wheel_builder.save_to(wheel_path)
+ return wheel_path
+
+ return _make_fake_wheel
+
+
def test_new_resolver_can_install(script):
create_basic_wheel_for_package(
script,
@@ -641,8 +661,8 @@ def test_new_resolver_constraint_no_specifier(script):
"Unnamed requirements are not allowed as constraints",
),
(
- "req @ https://example.com/dist.zip",
- "Links are not allowed as constraints",
+ "-e git+https://example.com/dist.git#egg=req",
+ "Editable requirements are not allowed as constraints",
),
(
"pkg[extra]",
@@ -1278,3 +1298,493 @@ def test_new_resolver_no_fetch_no_satisfying(script):
"myuberpkg",
)
assert "Processing " not in result.stdout, str(result)
+
+
+def test_new_resolver_does_not_install_unneeded_packages_with_url_constraint(script):
+ archive_path = create_basic_wheel_for_package(
+ script,
+ "installed",
+ "0.1.0",
+ )
+ not_installed_path = create_basic_wheel_for_package(
+ script,
+ "not_installed",
+ "0.1.0",
+ )
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("not_installed @ " + path_to_url(not_installed_path))
+
+ (script.scratch_path / "index").mkdir()
+ archive_path.rename(script.scratch_path / "index" / archive_path.name)
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path / "index",
+ "-c", constraints_file,
+ "installed"
+ )
+
+ assert_installed(script, installed="0.1.0")
+ assert_not_installed(script, "not_installed")
+
+
+def test_new_resolver_installs_packages_with_url_constraint(script):
+ installed_path = create_basic_wheel_for_package(
+ script,
+ "installed",
+ "0.1.0",
+ )
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("installed @ " + path_to_url(installed_path))
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "-c", constraints_file,
+ "installed"
+ )
+
+ assert_installed(script, installed="0.1.0")
+
+
+def test_new_resolver_reinstall_link_requirement_with_constraint(script):
+ installed_path = create_basic_wheel_for_package(
+ script,
+ "installed",
+ "0.1.0",
+ )
+
+ cr_file = script.scratch_path / "constraints.txt"
+ cr_file.write_text("installed @ " + path_to_url(installed_path))
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "-r", cr_file,
+ )
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "-c", cr_file,
+ "-r", cr_file,
+ )
+ # TODO: strengthen assertion to "second invocation does no work"
+ # I don't think this is true yet, but it should be in the future.
+
+ assert_installed(script, installed="0.1.0")
+
+
+def test_new_resolver_prefers_url_constraint(script):
+ installed_path = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.1.0",
+ )
+ not_installed_path = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.2.0",
+ )
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("test_pkg @ " + path_to_url(installed_path))
+
+ (script.scratch_path / "index").mkdir()
+ not_installed_path.rename(script.scratch_path / "index" / not_installed_path.name)
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path / "index",
+ "-c", constraints_file,
+ "test_pkg"
+ )
+
+ assert_installed(script, test_pkg="0.1.0")
+
+
+def test_new_resolver_prefers_url_constraint_on_update(script):
+ installed_path = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.1.0",
+ )
+ not_installed_path = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.2.0",
+ )
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("test_pkg @ " + path_to_url(installed_path))
+
+ (script.scratch_path / "index").mkdir()
+ not_installed_path.rename(script.scratch_path / "index" / not_installed_path.name)
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path / "index",
+ "test_pkg"
+ )
+
+ assert_installed(script, test_pkg="0.2.0")
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path / "index",
+ "-c", constraints_file,
+ "test_pkg"
+ )
+
+ assert_installed(script, test_pkg="0.1.0")
+
+
+@pytest.mark.parametrize("version_option", ["--constraint", "--requirement"])
+def test_new_resolver_fails_with_url_constraint_and_incompatible_version(
+ script, version_option,
+):
+ not_installed_path = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.1.0",
+ )
+ not_installed_path = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.2.0",
+ )
+
+ url_constraint = script.scratch_path / "constraints.txt"
+ url_constraint.write_text("test_pkg @ " + path_to_url(not_installed_path))
+
+ version_req = script.scratch_path / "requirements.txt"
+ version_req.write_text("test_pkg<0.2.0")
+
+ result = script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path,
+ "--constraint", url_constraint,
+ version_option, version_req,
+ "test_pkg",
+ expect_error=True,
+ )
+
+ assert "Cannot install test_pkg" in result.stderr, str(result)
+ assert (
+ "because these package versions have conflicting dependencies."
+ ) in result.stderr, str(result)
+
+ assert_not_installed(script, "test_pkg")
+
+ # Assert that pip works properly in the absence of the constraints file.
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path,
+ version_option, version_req,
+ "test_pkg"
+ )
+
+
+def test_new_resolver_ignores_unneeded_conflicting_constraints(script):
+ version_1 = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.1.0",
+ )
+ version_2 = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.2.0",
+ )
+ create_basic_wheel_for_package(
+ script,
+ "installed",
+ "0.1.0",
+ )
+
+ constraints = [
+ "test_pkg @ " + path_to_url(version_1),
+ "test_pkg @ " + path_to_url(version_2),
+ ]
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("\n".join(constraints))
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path,
+ "-c", constraints_file,
+ "installed"
+ )
+
+ assert_not_installed(script, "test_pkg")
+ assert_installed(script, installed="0.1.0")
+
+
+def test_new_resolver_fails_on_needed_conflicting_constraints(script):
+ version_1 = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.1.0",
+ )
+ version_2 = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.2.0",
+ )
+
+ constraints = [
+ "test_pkg @ " + path_to_url(version_1),
+ "test_pkg @ " + path_to_url(version_2),
+ ]
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("\n".join(constraints))
+
+ result = script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path,
+ "-c", constraints_file,
+ "test_pkg",
+ expect_error=True,
+ )
+
+ assert (
+ "Cannot install test_pkg because these package versions have conflicting "
+ "dependencies."
+ ) in result.stderr, str(result)
+
+ assert_not_installed(script, "test_pkg")
+
+ # Assert that pip works properly in the absence of the constraints file.
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path,
+ "test_pkg",
+ )
+
+
+def test_new_resolver_fails_on_conflicting_constraint_and_requirement(script):
+ version_1 = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.1.0",
+ )
+ version_2 = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.2.0",
+ )
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("test_pkg @ " + path_to_url(version_1))
+
+ result = script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path,
+ "-c", constraints_file,
+ "test_pkg @ " + path_to_url(version_2),
+ expect_error=True,
+ )
+
+ assert "Cannot install test-pkg 0.2.0" in result.stderr, str(result)
+ assert (
+ "because these package versions have conflicting dependencies."
+ ) in result.stderr, str(result)
+
+ assert_not_installed(script, "test_pkg")
+
+ # Assert that pip works properly in the absence of the constraints file.
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "--find-links", script.scratch_path,
+ "test_pkg @ " + path_to_url(version_2),
+ )
+
+
+@pytest.mark.parametrize("editable", [False, True])
+def test_new_resolver_succeeds_on_matching_constraint_and_requirement(script, editable):
+ if editable:
+ source_dir = create_test_package_with_setup(
+ script,
+ name="test_pkg",
+ version="0.1.0"
+ )
+ else:
+ source_dir = create_basic_wheel_for_package(
+ script,
+ "test_pkg",
+ "0.1.0",
+ )
+
+ req_line = "test_pkg @ " + path_to_url(source_dir)
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text(req_line)
+
+ if editable:
+ last_args = ("-e", source_dir)
+ else:
+ last_args = (req_line,)
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "-c", constraints_file,
+ *last_args,
+ )
+
+ assert_installed(script, test_pkg="0.1.0")
+ if editable:
+ assert_editable(script, "test-pkg")
+
+
+def test_new_resolver_applies_url_constraint_to_dep(script):
+ version_1 = create_basic_wheel_for_package(
+ script,
+ "dep",
+ "0.1.0",
+ )
+ version_2 = create_basic_wheel_for_package(
+ script,
+ "dep",
+ "0.2.0",
+ )
+
+ base = create_basic_wheel_for_package(script, "base", "0.1.0", depends=["dep"])
+
+ (script.scratch_path / "index").mkdir()
+ base.rename(script.scratch_path / "index" / base.name)
+ version_2.rename(script.scratch_path / "index" / version_2.name)
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("dep @ " + path_to_url(version_1))
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "-c", constraints_file,
+ "--find-links", script.scratch_path / "index",
+ "base",
+ )
+
+ assert_installed(script, dep="0.1.0")
+
+
+def test_new_resolver_handles_compatible_wheel_tags_in_constraint_url(
+ script, make_fake_wheel
+):
+ initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat")
+
+ constrained = script.scratch_path / "constrained"
+ constrained.mkdir()
+
+ final_path = constrained / initial_path.name
+
+ initial_path.rename(final_path)
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("base @ " + path_to_url(final_path))
+
+ result = script.pip(
+ "install",
+ "--implementation", "fakepy",
+ '--only-binary=:all:',
+ "--python-version", "1",
+ "--abi", "fakeabi",
+ "--platform", "fakeplat",
+ "--target", script.scratch_path / "target",
+ "--no-cache-dir", "--no-index",
+ "-c", constraints_file,
+ "base",
+ )
+
+ dist_info = Path("scratch", "target", "base-0.1.0.dist-info")
+ result.did_create(dist_info)
+
+
+def test_new_resolver_handles_incompatible_wheel_tags_in_constraint_url(
+ script, make_fake_wheel
+):
+ initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat")
+
+ constrained = script.scratch_path / "constrained"
+ constrained.mkdir()
+
+ final_path = constrained / initial_path.name
+
+ initial_path.rename(final_path)
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("base @ " + path_to_url(final_path))
+
+ result = script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "-c", constraints_file,
+ "base",
+ expect_error=True,
+ )
+
+ assert (
+ "Cannot install base because these package versions have conflicting "
+ "dependencies."
+ ) in result.stderr, str(result)
+
+ assert_not_installed(script, "base")
+
+
+def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url(
+ script, make_fake_wheel
+):
+ initial_path = make_fake_wheel("dep", "0.1.0", "fakepy1-fakeabi-fakeplat")
+
+ constrained = script.scratch_path / "constrained"
+ constrained.mkdir()
+
+ final_path = constrained / initial_path.name
+
+ initial_path.rename(final_path)
+
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("dep @ " + path_to_url(final_path))
+
+ index = script.scratch_path / "index"
+ index.mkdir()
+
+ index_dep = create_basic_wheel_for_package(script, "dep", "0.2.0")
+
+ base = create_basic_wheel_for_package(
+ script, "base", "0.1.0"
+ )
+ base_2 = create_basic_wheel_for_package(
+ script, "base", "0.2.0", depends=["dep"]
+ )
+
+ index_dep.rename(index / index_dep.name)
+ base.rename(index / base.name)
+ base_2.rename(index / base_2.name)
+
+ script.pip(
+ "install",
+ "--no-cache-dir", "--no-index",
+ "-c", constraints_file,
+ "--find-links", script.scratch_path / "index",
+ "base",
+ )
+
+ assert_installed(script, base="0.1.0")
+ assert_not_installed(script, "dep")
diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py
index 854b66418ae..02397616ab7 100644
--- a/tests/functional/test_new_resolver_hashes.py
+++ b/tests/functional/test_new_resolver_hashes.py
@@ -1,7 +1,9 @@
import collections
import hashlib
+import json
import pytest
+from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.utils.urls import path_to_url
from tests.lib import create_basic_sdist_for_package, create_basic_wheel_for_package
@@ -11,6 +13,30 @@
)
+def assert_installed(script, **kwargs):
+ ret = script.pip('list', '--format=json')
+ installed = set(
+ (canonicalize_name(val['name']), val['version'])
+ for val in json.loads(ret.stdout)
+ )
+ expected = set((canonicalize_name(k), v) for k, v in kwargs.items())
+ assert expected <= installed, \
+ "{!r} not all in {!r}".format(expected, installed)
+
+
+def assert_not_installed(script, *args):
+ ret = script.pip("list", "--format=json")
+ installed = set(
+ canonicalize_name(val["name"])
+ for val in json.loads(ret.stdout)
+ )
+ # None of the given names should be listed as installed, i.e. their
+ # intersection should be empty.
+ expected = set(canonicalize_name(k) for k in args)
+ assert not (expected & installed), \
+ "{!r} contained in {!r}".format(expected, installed)
+
+
def _create_find_links(script):
sdist_path = create_basic_sdist_for_package(script, "base", "0.1.0")
wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0")
@@ -204,3 +230,81 @@ def test_new_resolver_hash_intersect_empty_from_constraint(script):
"from some requirements."
)
assert message in result.stderr, str(result)
+
+
+@pytest.mark.parametrize("constrain_by_hash", [False, True])
+def test_new_resolver_hash_requirement_and_url_constraint_can_succeed(
+ script, constrain_by_hash,
+):
+ wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0")
+
+ wheel_hash = hashlib.sha256(wheel_path.read_bytes()).hexdigest()
+
+ requirements_txt = script.scratch_path / "requirements.txt"
+ requirements_txt.write_text(
+ """
+ base==0.1.0 --hash=sha256:{wheel_hash}
+ """.format(
+ wheel_hash=wheel_hash,
+ ),
+ )
+
+ constraints_txt = script.scratch_path / "constraints.txt"
+ constraint_text = "base @ {wheel_url}\n".format(wheel_url=path_to_url(wheel_path))
+ if constrain_by_hash:
+ constraint_text += "base==0.1.0 --hash=sha256:{wheel_hash}\n".format(
+ wheel_hash=wheel_hash,
+ )
+ constraints_txt.write_text(constraint_text)
+
+ script.pip(
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "--constraint", constraints_txt,
+ "--requirement", requirements_txt,
+ )
+
+ assert_installed(script, base="0.1.0")
+
+
+@pytest.mark.parametrize("constrain_by_hash", [False, True])
+def test_new_resolver_hash_requirement_and_url_constraint_can_fail(
+ script, constrain_by_hash,
+):
+ wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0")
+ other_path = create_basic_wheel_for_package(script, "other", "0.1.0")
+
+ other_hash = hashlib.sha256(other_path.read_bytes()).hexdigest()
+
+ requirements_txt = script.scratch_path / "requirements.txt"
+ requirements_txt.write_text(
+ """
+ base==0.1.0 --hash=sha256:{other_hash}
+ """.format(
+ other_hash=other_hash,
+ ),
+ )
+
+ constraints_txt = script.scratch_path / "constraints.txt"
+ constraint_text = "base @ {wheel_url}\n".format(wheel_url=path_to_url(wheel_path))
+ if constrain_by_hash:
+ constraint_text += "base==0.1.0 --hash=sha256:{other_hash}\n".format(
+ other_hash=other_hash,
+ )
+ constraints_txt.write_text(constraint_text)
+
+ result = script.pip(
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "--constraint", constraints_txt,
+ "--requirement", requirements_txt,
+ expect_error=True,
+ )
+
+ assert (
+ "THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE."
+ ) in result.stderr, str(result)
+
+ assert_not_installed(script, "base", "other")