Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Incorrect resolution for JAX #2371

Merged
merged 1 commit into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions news/2273.enhancement.md

This file was deleted.

1 change: 0 additions & 1 deletion news/2286.refactor.md

This file was deleted.

1 change: 1 addition & 0 deletions news/2369.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a resolution issue that extra dependencies are not resolved when the bare dependency has more specific version constraint.
5 changes: 5 additions & 0 deletions src/pdm/formats/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,18 @@ def export(
from pdm.models.candidates import Candidate

lines = ["# This file is @generated by PDM.\n# Please do not edit it manually.\n\n"]
collected_req: set[str] = set()
for candidate in sorted(candidates, key=lambda x: x.identify()): # type: ignore[attr-defined]
if isinstance(candidate, Candidate):
req = dataclasses.replace(candidate.req, specifier=get_specifier(f"=={candidate.version}"), marker=None)
else:
assert isinstance(candidate, Requirement)
req = candidate
line = project.backend.expand_line(req.as_line(), options.expandvars)
if line in collected_req:
continue
lines.append(project.backend.expand_line(req.as_line(), options.expandvars))
collected_req.add(line)
if options.hashes and getattr(candidate, "hashes", None):
for item in sorted({row["hash"] for row in candidate.hashes}): # type: ignore[attr-defined]
lines.append(f" \\\n --hash={item}")
Expand Down
10 changes: 10 additions & 0 deletions src/pdm/models/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,16 @@ def __init__(
def identify(self) -> str:
return self.req.identify()

def copy_with(self, requirement: Requirement) -> Candidate:
can = Candidate(requirement, name=self.name, version=self.version, link=self.link)
can.summary = self.summary
can.hashes = self.hashes
can._requires_python = self._requires_python
can._prepared = self._prepared
if can._prepared:
can._prepared.req = requirement
return can

@property
def dep_key(self) -> tuple[str, str | None]:
"""Key for retrieving and storing dependencies from the provider.
Expand Down
3 changes: 1 addition & 2 deletions src/pdm/models/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,8 +601,7 @@ def find_candidates(
if not requirement.name:
# make sure can.identify() won't return a randomly-generated name
requirement.name = can.name
can.req = requirement
yield can
yield can.copy_with(requirement)

def get_hashes(self, candidate: Candidate) -> list[FileHash]:
return candidate.hashes
15 changes: 10 additions & 5 deletions src/pdm/resolver/providers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import itertools
import os
from typing import TYPE_CHECKING, Callable, cast

Expand Down Expand Up @@ -159,15 +158,21 @@ def matches_gen() -> Iterator[Candidate]:
return (c for c in candidates if c not in incompat)
elif identifier in self.overrides:
return iter(self.get_override_candidates(identifier))
reqs_iter = requirements[identifier]
reqs = sorted(requirements[identifier], key=self.requirement_preference)
original_req = reqs[0]
bare_name, extras = strip_extras(identifier)
if extras and bare_name in requirements:
# We should consider the requirements for both foo and foo[extra]
reqs_iter = itertools.chain(reqs_iter, requirements[bare_name])
reqs = sorted(reqs_iter, key=self.requirement_preference)
reqs.extend(requirements[bare_name])
reqs.sort(key=self.requirement_preference)
candidates = self._find_candidates(reqs[0])
return (
can for can in candidates if can not in incompat and all(self.is_satisfied_by(r, can) for r in reqs)
# In some cases we will use candidates from the bare requirement,
# this will miss the extra dependencies if any. So we associate the original
# requirement back with the candidate since it is used by `get_dependencies()`.
can.copy_with(original_req) if extras else can
for can in candidates
if can not in incompat and all(self.is_satisfied_by(r, can) for r in reqs)
)

return matches_gen
Expand Down
8 changes: 8 additions & 0 deletions tests/cli/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,11 @@ def test_install_groups_and_lock(project, pdm, working_set):
assert project.lockfile.groups == ["tz"]
assert "pytz" in project.locked_repository.all_candidates
assert "urllib3" not in project.locked_repository.all_candidates


def test_install_requirement_with_extras(project, pdm, working_set):
project.add_dependencies({"requests": parse_requirement("requests==2.19.1")})
project.add_dependencies({"requests[socks]": parse_requirement("requests[socks]")}, to_group="socks")
pdm(["lock", "-Gsocks"], obj=project, strict=True)
pdm(["sync", "-Gsocks"], obj=project, strict=True)
assert "pysocks" in working_set