Skip to content

Commit 8fc65ea

Browse files
authored
Merge pull request #9673 from mwchase/url-constraints-final-2
Support URL constraints in the new resolver
2 parents cf2c2cc + 4c69ab2 commit 8fc65ea

File tree

12 files changed

+744
-26
lines changed

12 files changed

+744
-26
lines changed

docs/html/user_guide.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,11 @@ Constraints Files
254254

255255
Constraints files are requirements files that only control which version of a
256256
requirement is installed, not whether it is installed or not. Their syntax and
257-
contents is nearly identical to :ref:`Requirements Files`. There is one key
258-
difference: Including a package in a constraints file does not trigger
259-
installation of the package.
257+
contents is a subset of :ref:`Requirements Files`, with several kinds of syntax
258+
not allowed: constraints must have a name, they cannot be editable, and they
259+
cannot specify extras. In terms of semantics, there is one key difference:
260+
Including a package in a constraints file does not trigger installation of the
261+
package.
260262

261263
Use a constraints file like so:
262264

news/8253.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add the ability for the new resolver to process URL constraints.

src/pip/_internal/models/link.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,9 @@ def is_hash_allowed(self, hashes):
240240
assert self.hash is not None
241241

242242
return hashes.is_hash_allowed(self.hash_name, hex_digest=self.hash)
243+
244+
245+
# TODO: Relax this comparison logic to ignore, for example, fragments.
246+
def links_equivalent(link1, link2):
247+
# type: (Link, Link) -> bool
248+
return link1 == link2

src/pip/_internal/req/constructors.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,3 +468,19 @@ def install_req_from_parsed_requirement(
468468
user_supplied=user_supplied,
469469
)
470470
return req
471+
472+
473+
def install_req_from_link_and_ireq(link, ireq):
474+
# type: (Link, InstallRequirement) -> InstallRequirement
475+
return InstallRequirement(
476+
req=ireq.req,
477+
comes_from=ireq.comes_from,
478+
editable=ireq.editable,
479+
link=link,
480+
markers=ireq.markers,
481+
use_pep517=ireq.use_pep517,
482+
isolated=ireq.isolated,
483+
install_options=ireq.install_options,
484+
global_options=ireq.global_options,
485+
hash_options=ireq.hash_options,
486+
)

src/pip/_internal/req/req_install.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -848,8 +848,8 @@ def check_invalid_constraint_type(req):
848848
problem = ""
849849
if not req.name:
850850
problem = "Unnamed requirements are not allowed as constraints"
851-
elif req.link:
852-
problem = "Links are not allowed as constraints"
851+
elif req.editable:
852+
problem = "Editable requirements are not allowed as constraints"
853853
elif req.extras:
854854
problem = "Constraints cannot have extras"
855855

src/pip/_internal/resolution/resolvelib/base.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
55
from pip._vendor.packaging.version import LegacyVersion, Version
66

7-
from pip._internal.models.link import Link
7+
from pip._internal.models.link import Link, links_equivalent
88
from pip._internal.req.req_install import InstallRequirement
99
from pip._internal.utils.hashes import Hashes
1010

@@ -21,24 +21,26 @@ def format_name(project, extras):
2121

2222

2323
class Constraint:
24-
def __init__(self, specifier, hashes):
25-
# type: (SpecifierSet, Hashes) -> None
24+
def __init__(self, specifier, hashes, links):
25+
# type: (SpecifierSet, Hashes, FrozenSet[Link]) -> None
2626
self.specifier = specifier
2727
self.hashes = hashes
28+
self.links = links
2829

2930
@classmethod
3031
def empty(cls):
3132
# type: () -> Constraint
32-
return Constraint(SpecifierSet(), Hashes())
33+
return Constraint(SpecifierSet(), Hashes(), frozenset())
3334

3435
@classmethod
3536
def from_ireq(cls, ireq):
3637
# type: (InstallRequirement) -> Constraint
37-
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False))
38+
links = frozenset([ireq.link]) if ireq.link else frozenset()
39+
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links)
3840

3941
def __nonzero__(self):
4042
# type: () -> bool
41-
return bool(self.specifier) or bool(self.hashes)
43+
return bool(self.specifier) or bool(self.hashes) or bool(self.links)
4244

4345
def __bool__(self):
4446
# type: () -> bool
@@ -50,10 +52,16 @@ def __and__(self, other):
5052
return NotImplemented
5153
specifier = self.specifier & other.specifier
5254
hashes = self.hashes & other.hashes(trust_internet=False)
53-
return Constraint(specifier, hashes)
55+
links = self.links
56+
if other.link:
57+
links = links.union([other.link])
58+
return Constraint(specifier, hashes, links)
5459

5560
def is_satisfied_by(self, candidate):
5661
# type: (Candidate) -> bool
62+
# Reject if there are any mismatched URL constraints on this package.
63+
if self.links and not all(_match_link(link, candidate) for link in self.links):
64+
return False
5765
# We can safely always allow prereleases here since PackageFinder
5866
# already implements the prerelease logic, and would have filtered out
5967
# prerelease candidates if the user does not expect them.
@@ -95,6 +103,13 @@ def format_for_error(self):
95103
raise NotImplementedError("Subclass should override")
96104

97105

106+
def _match_link(link, candidate):
107+
# type: (Link, Candidate) -> bool
108+
if candidate.source_link:
109+
return links_equivalent(link, candidate.source_link)
110+
return False
111+
112+
98113
class Candidate:
99114
@property
100115
def project_name(self):

src/pip/_internal/resolution/resolvelib/candidates.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pip._vendor.pkg_resources import Distribution
1010

1111
from pip._internal.exceptions import HashError, MetadataInconsistent
12-
from pip._internal.models.link import Link
12+
from pip._internal.models.link import Link, links_equivalent
1313
from pip._internal.models.wheel import Wheel
1414
from pip._internal.req.constructors import (
1515
install_req_from_editable,
@@ -156,7 +156,7 @@ def __hash__(self):
156156
def __eq__(self, other):
157157
# type: (Any) -> bool
158158
if isinstance(other, self.__class__):
159-
return self._link == other._link
159+
return links_equivalent(self._link, other._link)
160160
return False
161161

162162
@property

src/pip/_internal/resolution/resolvelib/factory.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from pip._internal.models.link import Link
3535
from pip._internal.models.wheel import Wheel
3636
from pip._internal.operations.prepare import RequirementPreparer
37+
from pip._internal.req.constructors import install_req_from_link_and_ireq
3738
from pip._internal.req.req_install import InstallRequirement
3839
from pip._internal.resolution.base import InstallRequirementProvider
3940
from pip._internal.utils.compatibility_tags import get_supported
@@ -296,6 +297,46 @@ def find_candidates(
296297
if ireq is not None:
297298
ireqs.append(ireq)
298299

300+
for link in constraint.links:
301+
if not ireqs:
302+
# If we hit this condition, then we cannot construct a candidate.
303+
# However, if we hit this condition, then none of the requirements
304+
# provided an ireq, so they must have provided an explicit candidate.
305+
# In that case, either the candidate matches, in which case this loop
306+
# doesn't need to do anything, or it doesn't, in which case there's
307+
# nothing this loop can do to recover.
308+
break
309+
if link.is_wheel:
310+
wheel = Wheel(link.filename)
311+
# Check whether the provided wheel is compatible with the target
312+
# platform.
313+
if not wheel.supported(self._finder.target_python.get_tags()):
314+
# We are constrained to install a wheel that is incompatible with
315+
# the target architecture, so there are no valid candidates.
316+
# Return early, with no candidates.
317+
return ()
318+
# Create a "fake" InstallRequirement that's basically a clone of
319+
# what "should" be the template, but with original_link set to link.
320+
# Using the given requirement is necessary for preserving hash
321+
# requirements, but without the original_link, direct_url.json
322+
# won't be created.
323+
ireq = install_req_from_link_and_ireq(link, ireqs[0])
324+
candidate = self._make_candidate_from_link(
325+
link,
326+
extras=frozenset(),
327+
template=ireq,
328+
name=canonicalize_name(ireq.name) if ireq.name else None,
329+
version=None,
330+
)
331+
if candidate is None:
332+
# _make_candidate_from_link returns None if the wheel fails to build.
333+
# We are constrained to install this wheel, so there are no valid
334+
# candidates.
335+
# Return early, with no candidates.
336+
return ()
337+
338+
explicit_candidates.add(candidate)
339+
299340
# If none of the requirements want an explicit candidate, we can ask
300341
# the finder for candidates.
301342
if not explicit_candidates:

tests/functional/test_install_direct_url.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import re
22

3+
import pytest
4+
35
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
46
from tests.lib import _create_test_package, path_to_url
57

@@ -46,3 +48,24 @@ def test_install_archive_direct_url(script, data, with_wheel):
4648
assert req.startswith("simple @ file://")
4749
result = script.pip("install", req)
4850
assert _get_created_direct_url(result, "simple")
51+
52+
53+
@pytest.mark.network
54+
def test_install_vcs_constraint_direct_url(script, with_wheel):
55+
constraints_file = script.scratch_path / "constraints.txt"
56+
constraints_file.write_text(
57+
"git+https://github.com/pypa/pip-test-package"
58+
"@5547fa909e83df8bd743d3978d6667497983a4b7"
59+
"#egg=pip-test-package"
60+
)
61+
result = script.pip("install", "pip-test-package", "-c", constraints_file)
62+
assert _get_created_direct_url(result, "pip_test_package")
63+
64+
65+
def test_install_vcs_constraint_direct_file_url(script, with_wheel):
66+
pkg_path = _create_test_package(script, name="testpkg")
67+
url = path_to_url(pkg_path)
68+
constraints_file = script.scratch_path / "constraints.txt"
69+
constraints_file.write_text(f"git+{url}#egg=testpkg")
70+
result = script.pip("install", "testpkg", "-c", constraints_file)
71+
assert _get_created_direct_url(result, "testpkg")

tests/functional/test_install_reqs.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ def test_constraints_constrain_to_local_editable(
405405
expect_error=(resolver_variant == "2020-resolver"),
406406
)
407407
if resolver_variant == "2020-resolver":
408-
assert 'Links are not allowed as constraints' in result.stderr
408+
assert 'Editable requirements are not allowed as constraints' in result.stderr
409409
else:
410410
assert 'Running setup.py develop for singlemodule' in result.stdout
411411

@@ -419,12 +419,8 @@ def test_constraints_constrain_to_local(script, data, resolver_variant):
419419
'install', '--no-index', '-f', data.find_links, '-c',
420420
script.scratch_path / 'constraints.txt', 'singlemodule',
421421
allow_stderr_warning=True,
422-
expect_error=(resolver_variant == "2020-resolver"),
423422
)
424-
if resolver_variant == "2020-resolver":
425-
assert 'Links are not allowed as constraints' in result.stderr
426-
else:
427-
assert 'Running setup.py install for singlemodule' in result.stdout
423+
assert 'Running setup.py install for singlemodule' in result.stdout
428424

429425

430426
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):
438434
expect_error=(resolver_variant == "2020-resolver"),
439435
)
440436
if resolver_variant == "2020-resolver":
441-
assert 'Links are not allowed as constraints' in result.stderr
437+
assert 'Cannot install singlemodule 0.0.1' in result.stderr, str(result)
438+
assert (
439+
'because these package versions have conflicting dependencies.'
440+
in result.stderr
441+
), str(result)
442442
else:
443443
assert ('Running setup.py install for singlemodule'
444444
in result.stdout), str(result)
@@ -489,7 +489,7 @@ def test_install_with_extras_from_constraints(script, data, resolver_variant):
489489
expect_error=(resolver_variant == "2020-resolver"),
490490
)
491491
if resolver_variant == "2020-resolver":
492-
assert 'Links are not allowed as constraints' in result.stderr
492+
assert 'Constraints cannot have extras' in result.stderr
493493
else:
494494
result.did_create(script.site_packages / 'simple')
495495

@@ -521,7 +521,7 @@ def test_install_with_extras_joined(script, data, resolver_variant):
521521
expect_error=(resolver_variant == "2020-resolver"),
522522
)
523523
if resolver_variant == "2020-resolver":
524-
assert 'Links are not allowed as constraints' in result.stderr
524+
assert 'Constraints cannot have extras' in result.stderr
525525
else:
526526
result.did_create(script.site_packages / 'simple')
527527
result.did_create(script.site_packages / 'singlemodule.py')
@@ -538,7 +538,7 @@ def test_install_with_extras_editable_joined(script, data, resolver_variant):
538538
expect_error=(resolver_variant == "2020-resolver"),
539539
)
540540
if resolver_variant == "2020-resolver":
541-
assert 'Links are not allowed as constraints' in result.stderr
541+
assert 'Editable requirements are not allowed as constraints' in result.stderr
542542
else:
543543
result.did_create(script.site_packages / 'simple')
544544
result.did_create(script.site_packages / 'singlemodule.py')

0 commit comments

Comments
 (0)