diff --git a/news/12018.bugfix.rst b/news/12018.bugfix.rst new file mode 100644 index 00000000000..5e4f37f25a7 --- /dev/null +++ b/news/12018.bugfix.rst @@ -0,0 +1 @@ +Allow URL constraints to apply to requirements with extras. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index ede3e6b2b94..76edd8b458d 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -376,16 +376,28 @@ def _iter_candidates_from_constraints( This creates "fake" InstallRequirement objects that are basically clones of what "should" be the template, but with original_link set to link. """ + extras: frozenset[str] = frozenset() + base_identifier = identifier + with contextlib.suppress(InvalidRequirement): + parsed_requirement = get_requirement(identifier) + if parsed_requirement.name != identifier: + base_identifier = canonicalize_name(parsed_requirement.name) + extras = frozenset(parsed_requirement.extras) + for link in constraint.links: self._fail_if_link_is_unsupported_wheel(link) - candidate = self._make_base_candidate_from_link( + base_candidate = self._make_base_candidate_from_link( link, template=install_req_from_link_and_ireq(link, template), - name=canonicalize_name(identifier), + name=canonicalize_name(base_identifier), version=None, ) - if candidate: - yield candidate + if base_candidate is None: + continue + if extras: + yield self._make_extras_candidate(base_candidate, extras) + else: + yield base_candidate def find_candidates( self, diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 409fda88051..298dd8b9f6b 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -680,6 +680,35 @@ def test_install_with_extras_from_install(script: PipTestEnvironment) -> None: result.did_create(script.site_packages / "singlemodule.py") +def test_install_with_extras_and_url_constraint( + script: PipTestEnvironment, +) -> None: + """Regression test for https://github.com/pypa/pip/issues/12018. + + A URL constraint for the base package plus a requirement that asks for + the same package with extras used to trigger an AssertionError in + LinkCandidate (``'name[extra]' != 'name' for wheel``). + """ + create_basic_wheel_for_package( + script, + name="LocalExtras", + version="0.0.1", + extras={"baz": ["singlemodule"]}, + ) + wheel_path = next(script.scratch_path.glob("LocalExtras-0.0.1-*.whl")) + script.scratch_path.joinpath("constraints.txt").write_text( + f"LocalExtras @ {wheel_path.as_uri()}" + ) + result = script.pip_install_local( + "--find-links", + script.scratch_path, + "-c", + script.scratch_path / "constraints.txt", + "LocalExtras[baz]", + ) + result.did_create(script.site_packages / "singlemodule.py") + + def test_install_with_extras_joined( script: PipTestEnvironment, data: TestData, resolver_variant: ResolverVariant ) -> None: