1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00

Support URL constraints in the new resolver

Fixes #8253
This commit is contained in:
Max W Chase 2021-01-17 23:01:24 -05:00
parent d150cf2ae4
commit 4c69ab2a2c
12 changed files with 744 additions and 26 deletions

View file

@ -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:

1
news/8253.feature.rst Normal file
View file

@ -0,0 +1 @@
Add the ability for the new resolver to process URL constraints.

View file

@ -240,3 +240,9 @@ class Link(KeyBasedCompareMixin):
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

View file

@ -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,
)

View file

@ -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"

View file

@ -4,7 +4,7 @@ from pip._vendor.packaging.specifiers import SpecifierSet
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 @@ class Constraint:
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 @@ class Requirement:
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):

View file

@ -8,7 +8,7 @@ from pip._vendor.packaging.version import Version, _BaseVersion
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 @@ class _InstallRequirementBackedCandidate(Candidate):
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

View file

@ -32,6 +32,7 @@ from pip._internal.index.package_finder import PackageFinder
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 @@ class Factory:
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:

View file

@ -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")

View file

@ -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')

View file

@ -10,7 +10,9 @@ from tests.lib import (
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")

View file

@ -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 @@ _FindLinks = collections.namedtuple(
)
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")