fix(show): respect active extras when showing a group's dependency tree - #10966
fix(show): respect active extras when showing a group's dependency tree#10966apoorva-01 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The extras filtering logic is duplicated in both
display_package_treeand_display_tree; consider extracting this into a small helper function to keep the behavior consistent and easier to maintain. - When computing
active_extrasin_display_packages_tree_information, using a set of extras from all matching root requires changes the behavior if the same package is declared with different extras in the same group; if that’s undesirable, you might want to choose a single require or preserve per-edge extras instead of merging them.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The extras filtering logic is duplicated in both `display_package_tree` and `_display_tree`; consider extracting this into a small helper function to keep the behavior consistent and easier to maintain.
- When computing `active_extras` in `_display_packages_tree_information`, using a set of extras from all matching root requires changes the behavior if the same package is declared with different extras in the same group; if that’s undesirable, you might want to choose a single require or preserve per-edge extras instead of merging them.
## Individual Comments
### Comment 1
<location path="src/poetry/console/commands/show.py" line_range="529-532" />
<code_context>
package: Package,
installed_packages: list[Package],
why_package: Package | None = None,
+ active_extras: Collection[NormalizedName] | None = None,
) -> None:
io.write(f"<c1>{package.pretty_name}</c1>")
</code_context>
<issue_to_address>
**suggestion:** The extras filtering logic is duplicated and could be centralized to reduce drift and subtle differences.
`dependencies` are filtered by `active_extras` in both `display_package_tree` and `_display_tree` using the same condition (`not d.in_extras or any(extra in active_extras for extra in d.in_extras)`). Duplicating this logic makes future changes (e.g., case-normalization or more complex extra semantics) harder to keep consistent. Consider extracting it into a helper like `_filter_dependencies_by_extras(dependencies, active_extras)` to centralize and simplify maintenance.
Suggested implementation:
```python
return 0
def _filter_dependencies_by_extras(
self,
dependencies: Collection[Package],
active_extras: Collection[NormalizedName] | None,
) -> list[Package]:
if active_extras is None:
return list(dependencies)
return [
d
for d in dependencies
if not d.in_extras
or any(extra in active_extras for extra in d.in_extras)
]
package: Package,
installed_packages: list[Package],
why_package: Package | None = None,
active_extras: Collection[NormalizedName] | None = None,
) -> None:
```
```python
io.write(f"<c1>{package.pretty_name}</c1>")
description = ""
dependencies = [p for p in package.requires if p.name == why_package.name]
else:
dependencies = self._filter_dependencies_by_extras(
package.requires, active_extras
)
```
You mentioned the same extras filtering logic is also present in `display_package_tree`. Once you locate that block (it should look like the `if not d.in_extras or any(extra in active_extras for extra in d.in_extras)` comprehension over `dependencies`), replace it with a call to `self._filter_dependencies_by_extras(...)` as well, e.g.:
```python
dependencies = self._filter_dependencies_by_extras(
package.requires, active_extras
)
```
Also, adjust the type annotations for `_filter_dependencies_by_extras`’s `dependencies` parameter if the actual dependency type in this file is not `Package` (e.g., if it’s a `Dependency`/`Requirement` type), to stay consistent with the existing codebase.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| active_extras: Collection[NormalizedName] | None = None, | ||
| ) -> None: | ||
| io.write(f"<c1>{package.pretty_name}</c1>") | ||
| description = "" |
There was a problem hiding this comment.
suggestion: The extras filtering logic is duplicated and could be centralized to reduce drift and subtle differences.
dependencies are filtered by active_extras in both display_package_tree and _display_tree using the same condition (not d.in_extras or any(extra in active_extras for extra in d.in_extras)). Duplicating this logic makes future changes (e.g., case-normalization or more complex extra semantics) harder to keep consistent. Consider extracting it into a helper like _filter_dependencies_by_extras(dependencies, active_extras) to centralize and simplify maintenance.
Suggested implementation:
return 0
def _filter_dependencies_by_extras(
self,
dependencies: Collection[Package],
active_extras: Collection[NormalizedName] | None,
) -> list[Package]:
if active_extras is None:
return list(dependencies)
return [
d
for d in dependencies
if not d.in_extras
or any(extra in active_extras for extra in d.in_extras)
]
package: Package,
installed_packages: list[Package],
why_package: Package | None = None,
active_extras: Collection[NormalizedName] | None = None,
) -> None: io.write(f"<c1>{package.pretty_name}</c1>")
description = ""
dependencies = [p for p in package.requires if p.name == why_package.name]
else:
dependencies = self._filter_dependencies_by_extras(
package.requires, active_extras
)You mentioned the same extras filtering logic is also present in display_package_tree. Once you locate that block (it should look like the if not d.in_extras or any(extra in active_extras for extra in d.in_extras) comprehension over dependencies), replace it with a call to self._filter_dependencies_by_extras(...) as well, e.g.:
dependencies = self._filter_dependencies_by_extras(
package.requires, active_extras
)Also, adjust the type annotations for _filter_dependencies_by_extras’s dependencies parameter if the actual dependency type in this file is not Package (e.g., if it’s a Dependency/Requirement type), to stay consistent with the existing codebase.
show --tree walked each package's full requires, including deps gated behind extras the group never activates, so --tree --only <group> listed another group's extra deps. Filter to active extras, like the solver.
97c94be to
c0141e5
Compare
| dependencies = [ | ||
| d | ||
| for d in dependencies | ||
| if not d.in_extras |
There was a problem hiding this comment.
in_extras loses the marker’s boolean structure here. A dependency marked extra == "foo" or python_version >= "3.10" has in_extras == ["foo"], so --only without that extra now hides it even on Python 3.12 where the full marker is true.
Pull Request Check List
Resolves: #10416
Problem
poetry show --tree --only <group>can show another group's dependencies. If a package is required by two groups with different extras, one group's tree lists the other's extra deps.show --only bcorrectly omitsemail-validator;--tree --only bshows it.Fix
The flat output already respects active groups and their extras; the tree didn't. It walked each package's full
requires, including deps gated behind extras the group never turned on. The tree now filters with the same rule the solver uses (a dep shows only if it isn't extra-gated, or one of its gating extras is active), carrying active extras down each edge.display_package_tree/_display_treetake a newactive_extrasarg defaulting toNone, so plainshow <pkg> --treeis unchanged.Tested
test_show_tree_only_group_respects_active_extras: same package in two groups, one with an extra, asserts the extra's dep is absent without it and present with it (reverting fails the first check). Fulltest_show.pypasses; mypy and ruff clean. Scope: extras only, non-extra environment markers stay unfiltered (pre-existing).