Skip to content

Commit 34e754e

Browse files
Solaris-starpre-commit-ci[bot]frostming
authored
fix(workspace): keep declared group for workspace members in lockfile (#3819)
* fix(workspace): keep declared group for workspace members in lockfile Implicit workspace members were always injected into the default group with groups=["default"]. When a member was only declared in a dev/test group, lockfile metadata still listed default, which is wrong. Skip members already present in non-default groups when injecting implicit workspace deps, and associate the original requirement when resolving workspace candidates. Fixes #3816 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(workspace): preserve editable candidates and lock groups Signed-off-by: Solaris-star <820622658@qq.com> --------- Signed-off-by: Solaris-star <820622658@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: frostming <me@frostming.com>
1 parent 7cb8367 commit 34e754e

5 files changed

Lines changed: 59 additions & 8 deletions

File tree

news/3816.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Keep workspace members in their explicitly declared dependency groups instead of adding them to ``default`` in the lockfile.

src/pdm/project/core.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import os
88
import shutil
99
import sys
10-
from collections.abc import Callable, Iterable, Mapping, Sequence
10+
from collections.abc import Callable, Collection, Iterable, Mapping, Sequence
1111
from copy import deepcopy
1212
from functools import cached_property, reduce
1313
from pathlib import Path
@@ -161,9 +161,19 @@ def iter_workspace_dependencies(self) -> Iterable[Requirement]:
161161
"""Iterate over implicit editable requirements for workspace members."""
162162
yield from self.workspace.iter_dependencies()
163163

164-
def with_workspace_dependencies(self, requirements: Iterable[Requirement]) -> list[Requirement]:
165-
"""Return requirements with implicit workspace member dependencies included."""
166-
return self.workspace.with_dependencies(requirements)
164+
def with_workspace_dependencies(
165+
self,
166+
requirements: Iterable[Requirement],
167+
*,
168+
exclude: Collection[str] | None = None,
169+
) -> list[Requirement]:
170+
"""Return requirements with implicit workspace member dependencies included.
171+
172+
Members already declared in other dependency groups are skipped so they
173+
keep those groups in the lockfile instead of being forced into
174+
``default`` (#3816).
175+
"""
176+
return self.workspace.with_dependencies(requirements, exclude=exclude)
167177

168178
def pyproject_content_hash(self, algo: str = "sha256") -> str:
169179
"""Return a lockfile content hash including workspace members."""
@@ -554,7 +564,12 @@ def _get_dependencies(group: str) -> tuple[list[Requirement], set[str]]:
554564
for group, deps in extra_deps.items():
555565
group_deps[group].extend(deps)
556566
if "default" in group_deps and self.is_workspace_root:
557-
group_deps["default"] = self.with_workspace_dependencies(group_deps["default"])
567+
# Do not force workspace members that are already declared in other
568+
# groups into default (keeps lockfile groups accurate) (#3816).
569+
declared_elsewhere = {
570+
req.identify() for group, deps in group_deps.items() if group != "default" for req in deps
571+
}
572+
group_deps["default"] = self.with_workspace_dependencies(group_deps["default"], exclude=declared_elsewhere)
558573
return group_deps
559574

560575
@property

src/pdm/project/workspace.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import hashlib
4-
from collections.abc import Iterable
4+
from collections.abc import Collection, Iterable
55
from functools import cached_property
66
from pathlib import Path
77
from typing import TYPE_CHECKING
@@ -87,9 +87,16 @@ def iter_dependencies(self) -> Iterable[Requirement]:
8787
req.groups = ["default"]
8888
yield req
8989

90-
def with_dependencies(self, requirements: Iterable[Requirement]) -> list[Requirement]:
90+
def with_dependencies(
91+
self,
92+
requirements: Iterable[Requirement],
93+
*,
94+
exclude: Collection[str] | None = None,
95+
) -> list[Requirement]:
9196
result = list(requirements)
9297
seen = {req.identify() for req in result}
98+
if exclude:
99+
seen.update(exclude)
93100
for req in self.iter_dependencies():
94101
if req.identify() in seen:
95102
continue

src/pdm/resolver/providers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ def matches_gen() -> Iterator[Candidate]:
312312
reqs.sort(key=self.requirement_preference)
313313
if workspace_candidates := list(self._find_workspace_candidates(identifier)):
314314
return (
315-
can
315+
can.copy_with(dataclasses.replace(can.req, groups=original_req.groups))
316316
for can in workspace_candidates
317317
if can not in incompat and all(self.is_satisfied_by(r, can) for r in reqs)
318318
)

tests/test_project.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,34 @@ def test_workspace_root_adds_members_as_implicit_editable_dependencies(project):
110110
assert member_dependency.str_path == "./packages/foo"
111111

112112

113+
def test_workspace_member_declared_in_dev_group_not_forced_into_default(project, pdm):
114+
"""Workspace members already in other groups must not be re-added to default (#3816)."""
115+
project.pyproject.settings["workspace"] = {"members": ["packages/*"]}
116+
project.pyproject.write()
117+
member_path = project.root / "packages" / "foo"
118+
member_path.mkdir(parents=True)
119+
member_path.joinpath("pyproject.toml").write_text(
120+
'[project]\nname = "foo"\nversion = "0.1.0"\n',
121+
encoding="utf-8",
122+
)
123+
# Declare the member only in a dev/test group, not in main dependencies.
124+
project.add_dependencies(
125+
[parse_requirement("foo @ file:///${PROJECT_ROOT}/packages/foo", True)],
126+
"test",
127+
True,
128+
)
129+
130+
all_deps = project._resolve_dependencies()
131+
default_deps = project.get_dependencies("default", all_deps)
132+
assert all(getattr(dep, "name", None) != "foo" for dep in default_deps)
133+
134+
test_deps = project.get_dependencies("test", all_deps)
135+
assert any(getattr(dep, "name", None) == "foo" for dep in test_deps)
136+
137+
pdm(["lock"], obj=project, strict=True)
138+
assert project.get_locked_repository().candidates["foo"].req.groups == ["test"]
139+
140+
113141
def test_add_member_to_workspace(project):
114142
project.pyproject.settings["workspace"] = {"members": []}
115143
project.pyproject.write()

0 commit comments

Comments
 (0)