Skip to content

fix(show): respect active extras when showing a group's dependency tree - #10966

Open
apoorva-01 wants to merge 1 commit into
python-poetry:mainfrom
apoorva-01:fix/show-tree-only-group-extras
Open

fix(show): respect active extras when showing a group's dependency tree#10966
apoorva-01 wants to merge 1 commit into
python-poetry:mainfrom
apoorva-01:fix/show-tree-only-group-extras

Conversation

@apoorva-01

@apoorva-01 apoorva-01 commented Jun 29, 2026

Copy link
Copy Markdown

Pull Request Check List

Resolves: #10416

  • Added tests for changed code.
  • Updated documentation for changed code.

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 b correctly omits email-validator; --tree --only b shows 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_tree take a new active_extras arg defaulting to None, so plain show <pkg> --tree is 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). Full test_show.py passes; mypy and ruff clean. Scope: extras only, non-extra environment markers stay unfiltered (pre-existing).

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +529 to 532
active_extras: Collection[NormalizedName] | None = None,
) -> None:
io.write(f"<c1>{package.pretty_name}</c1>")
description = ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@apoorva-01
apoorva-01 force-pushed the fix/show-tree-only-group-extras branch from 97c94be to c0141e5 Compare July 2, 2026 02:53
dependencies = [
d
for d in dependencies
if not d.in_extras

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

poetry show --tree --only group_A shows wrong tree when the same package has different extra dependencies in two different groups

2 participants