✨ feat(extras): add --extras flag for optional deps#549
✨ feat(extras): add --extras flag for optional deps#549gaborbernat merged 4 commits intotox-dev:mainfrom
Conversation
pipdeptree ignores extra/optional dependencies (PEP 508 extra markers), causing packages installed via `pip install pkg[extra]` to appear as unrelated top-level packages instead of children. This has been a long-standing request (issue tox-dev#107). The new `--extras`/`-x` flag enables transitive resolution of extras in the dependency tree. It collects extras from requirement specifiers (e.g. `oauthlib[signedtoken]`), evaluates markers with extras context, and includes root packages' provided extras where deps are installed. Extra dependencies are annotated in all output formats (text, JSON, graphviz, mermaid) for clarity.
|
This is great, I'll review it as soon as I can |
|
From a couple of passthroughs of the changes, this looks great. All the rendering options appear fine when testing. Tomorrow I plan to review and test it some more just to see if I can catch any issues. |
kemzeb
left a comment
There was a problem hiding this comment.
After further review, I wasn't able to find any glaring issues.
The only thing I did find was with the cyclic dependency validation check:

It maybe confusing as at first glance it looks like the dependency cycle is between required dependencies, when it is actually is between required and optional deps:
To get the changes above I made changes to render_cycles_text using the following:
# . . .
for idx, pkg in enumerate(cycle):
name = pkg.project_name
if isinstance(pkg, ReqPackage):
name = name + f" [extra: {pkg.extra}]" if pkg.extra else ""
if idx == size:
print(name, end="", file=sys.stderr) # noqa: T201
else:
print(f"{name} =>", end=" ", file=sys.stderr) # noqa: T201
# . . .You could use this or find a better alternative if you wish.
Cycle warnings didn't distinguish between required and optional deps, making it look like a required-dependency cycle when the cycle actually involves an extras edge. Annotate ReqPackage nodes that carry an extra with `[extra: ...]` in the cycle output.
|
Yep no problem! I did decide to mess around with projects with some complex extras, and found meta-package-manager. Did find some things:
|
|
I'll need to do some more digging into |
Interesting, so it appears that though the
I think this issue and the issue above have the same problem. I believe it's because we are trying to grab all extras when we should be grabbing only extras that were explicitly requested (so a requirement specifier like If you take a look at some of the optional versions of
Forget about this, it's an optional dependency. |
|
So what I think we need to figure out now is how to consider only explicit extras |
Extras are resolved from two sources: explicit requirement specifiers (e.g. pkg[extra]) and satisfied extras where all dependencies of an extra group are already installed in the environment.
63199eb to
8f56475
Compare
I think we should show an extra as installed if all its dependencies are available. This is how packages handle features, if a given extra package is importable is considered feature enabled, independent if was installed directly or through an additional dependency/install. |
kemzeb
left a comment
There was a problem hiding this comment.
This is good, I believe your approach makes sense. It's a shame there's currently no officially supported way to detect what non-explicit extras have been requested in the environment, but I can see why it is a complicated problem.
54df85e to
494514e
Compare
494514e to
7fc0b63
Compare
kemzeb
left a comment
There was a problem hiding this comment.
Not able to find any further issues.
However, I'm not sure if it would be worth it to account for optional dependencies in our DAG validation phase. For example:
Warning!!! Possibly conflicting dependencies found:
* graphviz==0.21
- pytest [required: >=7,<8.1, installed: 9.0.2, extra: test]
* pyproject-api==1.10.0
- setuptools [required: >=80.9, installed: 78.1.1, extra: testing]
* rdflib==7.6.0
- lxml [required: >=4.3,<6.0, installed: 6.0.2, extra: lxml]
------------------------------------------------------------------------
Warning!!! Cyclic dependencies found:
* pytest => exceptiongroup => pytest [extra: test]
* tox => pluggy => tox [extra: dev]
* pytest-cov => pluggy => tox [extra: dev] => pyproject-api => pytest-cov [extra: testing]
* pytest-mock => pytest => pluggy => tox [extra: dev] => pyproject-api => pytest-mock [extra: testing]
* setuptools => pytest-cov [extra: cover] => pluggy => tox [extra: dev] => pyproject-api => setuptools [extra: testing]
* exceptiongroup => pytest [extra: test] => exceptiongroup
* pyproject-api => pytest-cov [extra: testing] => pluggy => tox [extra: dev] => pyproject-api
* pluggy => tox [extra: dev] => pluggy
------------------------------------------------------------------------
Yes this logic for both detecting conflicting and cyclic dependencies is correct, but is it useful to include optional dependencies? Let's look at this line for example:
* pluggy => tox [extra: dev] => pluggy
pluggy uses tox in it's development workflow and tox uses pluggy for its plugin system, so technically it is a cyclic dependency but is this useful information? I won't block this since it is still technically correct

pipdeptreehas long ignored extra/optional dependencies defined via PEP 508extramarkers. When a user installs a package with extras (e.g.pip install jira[signedtoken]), the extra dependencies appear as unrelated top-level packages instead of showing up as children in the dependency tree. This has been a recurring pain point since issue #107 was opened, and previous attempts (#132, #138) were never merged.The new
--extras/-xflag enables transitive resolution of extras in the dependency tree. 🔍 It works by collecting extras from requirement specifiers (e.g.oauthlib[signedtoken]), evaluating PEP 508 markers with the correct extras context viamarker.evaluate({"extra": name}), and iterating until no new extras are discovered. Each extra dependency is annotated with its extra name across all output formats — text showsextra: signedtoken, JSON includes an"extra"field, and graphviz/mermaid prefix edge labels with[signedtoken]. Cycle warnings also annotate extras with[extra: ...]so it's clear when a cycle involves optional vs required deps.Only explicitly requested extras are resolved — those appearing in requirement specifiers like
cyclonedx-python-lib[validation]. Root packages' own extras are not auto-included since pip doesn't record which extras were used during installation, and heuristics produce false positives for forwarding-style extras (e.g.meta-package-managerforwards its extras toclick-extra). Validated againstmeta-package-manager[yaml,hjson]as a real-world test case.The flag is off by default to preserve backward compatibility. The
from_pkgsconstructor gained a keyword-onlyinclude_extrasparameter, and the extras resolution runs as a separate pass after the base DAG is built.