mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
parent
d150cf2ae4
commit
4c69ab2a2c
12 changed files with 744 additions and 26 deletions
|
@ -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
1
news/8253.feature.rst
Normal file
|
@ -0,0 +1 @@
|
|||
Add the ability for the new resolver to process URL constraints.
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue