Skip to content
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: 1 addition & 1 deletion .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
with:
enable-cache: false
- name: Install resolver dependencies
run: uv pip install --target .python-packages 'nab-index>=0.0.11' 'nab-project>=0.0.14,<0.0.16'
run: uv pip install --target .python-packages 'nab>=0.0.17' 'nab-index>=0.0.17' 'nab-project>=0.0.17'
- name: Check formatting
run: cargo fmt --all --check
- name: Check lints
Expand Down
1 change: 1 addition & 0 deletions docs/changelog/697.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix ``from-index`` with nab 0.0.17 by adapting to its configuration and resolver APIs.
55 changes: 35 additions & 20 deletions docs/how-to/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ A single requirement resolves to its full tree:
.. code-block:: console

$ pipdeptree from-index "starlette"
starlette==1.3.1
└── anyio [candidate: 4.14.2]
└── idna [candidate: 3.18]
starlette==1.6.0
└── anyio [candidate: 4.15.1]
├── idna [candidate: 3.19]
└── typing-extensions [candidate: 4.16.0]

Several requirements resolve together into one graph, and a version specifier bounds the pick:

Expand All @@ -65,16 +66,17 @@ Several requirements resolve together into one graph, and a version specifier bo

$ pipdeptree from-index "fastapi<=0.115.2" starlette
fastapi==0.115.2
├── pydantic [candidate: 2.13.4]
│ ├── annotated-types [candidate: 0.7.0]
│ ├── pydantic-core [candidate: 2.46.4]
├── pydantic [candidate: 2.13.5]
│ ├── annotated-types [candidate: 0.8.0]
│ ├── pydantic-core [candidate: 2.46.5]
│ │ └── typing-extensions [candidate: 4.16.0]
│ ├── typing-extensions [candidate: 4.16.0]
│ └── typing-inspection [candidate: 0.4.2]
│ └── typing-inspection [candidate: 0.4.4]
│ └── typing-extensions [candidate: 4.16.0]
├── starlette [candidate: 0.40.0]
│ └── anyio [candidate: 4.14.2]
│ └── idna [candidate: 3.18]
│ └── anyio [candidate: 4.15.1]
│ ├── idna [candidate: 3.19]
│ └── typing-extensions [candidate: 4.16.0]
└── typing-extensions [candidate: 4.16.0]

Request extras with the ``name[extra]`` syntax. The resolver pulls the extra's dependencies into the tree. They appear
Expand All @@ -85,9 +87,9 @@ as children, such as ``pysocks`` below, with the pinned version from the resolve

$ pipdeptree from-index "requests[socks]"
requests==2.34.2
├── certifi [candidate: 2026.6.17]
├── charset-normalizer [candidate: 3.4.9]
├── idna [candidate: 3.18]
├── certifi [candidate: 2026.7.22]
├── charset-normalizer [candidate: 3.5.1]
├── idna [candidate: 3.19]
├── pysocks [candidate: 1.7.1]
└── urllib3 [candidate: 2.7.0]

Expand All @@ -98,7 +100,7 @@ requirement; a non-matching marker drops it. Quote the argument so the shell kee
.. code-block:: console

$ pipdeptree from-index 'idna; python_version >= "3.10"'
idna==3.18
idna==3.19

Resolve from a requirements file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -195,37 +197,50 @@ The graph and render flags behave as they do for the default command. Emit JSON
"package": {
"key": "anyio",
"package_name": "anyio",
"candidate_version": "4.14.2"
"candidate_version": "4.15.1"
},
"dependencies": [
{
"key": "idna",
"package_name": "idna",
"candidate_version": "3.18"
"candidate_version": "3.19"
},
{
"key": "typing-extensions",
"package_name": "typing-extensions",
"candidate_version": "4.16.0"
}
]
},
{
"package": {
"key": "idna",
"package_name": "idna",
"candidate_version": "3.18"
"candidate_version": "3.19"
},
"dependencies": []
},
{
"package": {
"key": "starlette",
"package_name": "starlette",
"candidate_version": "1.3.1"
"candidate_version": "1.6.0"
},
"dependencies": [
{
"key": "anyio",
"package_name": "anyio",
"candidate_version": "4.14.2"
"candidate_version": "4.15.1"
}
]
},
{
"package": {
"key": "typing-extensions",
"package_name": "typing-extensions",
"candidate_version": "4.16.0"
},
"dependencies": []
}
]

Expand All @@ -235,8 +250,8 @@ Trace why the resolver pulled a package in with ``--reverse`` (``-r``):
.. code-block:: console

$ pipdeptree from-index "fastapi<=0.115.2" --reverse --packages anyio
anyio==4.14.2
└── starlette==0.40.0 [requires: anyio==4.14.2]
anyio==4.15.1
└── starlette==0.40.0 [requires: anyio==4.15.1]
└── fastapi==0.115.2 [requires: starlette==0.40.0]

Other supported flags include ``-o mermaid``, the ``graphviz-*`` formats, ``--depth`` (``-d``), package filters,
Expand Down
20 changes: 11 additions & 9 deletions docs/tutorial/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,10 @@ Ask what ``starlette`` brings along:
.. code-block:: console

$ pipdeptree from-index "starlette"
starlette==1.3.1
└── anyio [candidate: 4.14.2]
└── idna [candidate: 3.18]
starlette==1.6.0
└── anyio [candidate: 4.15.1]
├── idna [candidate: 3.19]
└── typing-extensions [candidate: 4.16.0]

Read this the same way as the tree from your environment. The top line is the requirement you asked for and the
indented lines are its dependencies. Each edge shows the candidate version the resolver selected from PyPI:
Expand All @@ -226,16 +227,17 @@ pin or bound it. Bound ``fastapi`` and resolve it alongside ``starlette``; the r

$ pipdeptree from-index "fastapi<=0.115.2" starlette
fastapi==0.115.2
├── pydantic [candidate: 2.13.4]
│ ├── annotated-types [candidate: 0.7.0]
│ ├── pydantic-core [candidate: 2.46.4]
├── pydantic [candidate: 2.13.5]
│ ├── annotated-types [candidate: 0.8.0]
│ ├── pydantic-core [candidate: 2.46.5]
│ │ └── typing-extensions [candidate: 4.16.0]
│ ├── typing-extensions [candidate: 4.16.0]
│ └── typing-inspection [candidate: 0.4.2]
│ └── typing-inspection [candidate: 0.4.4]
│ └── typing-extensions [candidate: 4.16.0]
├── starlette [candidate: 0.40.0]
│ └── anyio [candidate: 4.14.2]
│ └── idna [candidate: 3.18]
│ └── anyio [candidate: 4.15.1]
│ ├── idna [candidate: 3.19]
│ └── typing-extensions [candidate: 4.16.0]
└── typing-extensions [candidate: 4.16.0]

The resolver selects ``0.115.2`` for the top-level package and follows its dependencies. You can resolve a
Expand Down
19 changes: 10 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[build-system]
build-backend = "mesonpy"
requires = [
"meson>=1.11.2",
"meson-python>=0.20",
"ninja>=1.13",
"meson>=1.12",
"meson-python>=0.21",
"ninja>=1.13.2",
]

[project]
Expand Down Expand Up @@ -45,8 +45,9 @@ dynamic = [
"version",
]
dependencies = [
"nab-index>=0.0.11",
"nab-project<0.0.16,>=0.0.14",
"nab>=0.0.17",
"nab-index>=0.0.17",
"nab-project>=0.0.17",
]
urls.Changelog = "https://pipdeptree.readthedocs.io/en/latest/changelog.html"
urls.Documentation = "https://pipdeptree.readthedocs.io"
Expand All @@ -58,7 +59,7 @@ scripts.pipdeptree = "pipdeptree._runner:main"
[dependency-groups]
test = [
"covdefaults>=2.3",
"diff-cover>=10.3",
"diff-cover>=10.5.1",
"pluggy>=1.6",
"pytest>=9.1.1",
"pytest-cov>=7.1",
Expand All @@ -68,10 +69,10 @@ docs = [
"sphinx>=9.1",
"sphinx-copybutton>=0.5.2",
"sphinx-inline-tabs>=2025.12.21.14",
"sphinx-llm>=0.4.1",
"sphinxcontrib-mermaid>=2.1",
"sphinx-llm>=1",
"sphinxcontrib-mermaid>=2.1.1",
"sphinxcontrib-towncrier>=0.5.0a0",
"towncrier>=25.8",
"towncrier>=26.9",
]

[tool.meson-python]
Expand Down
13 changes: 7 additions & 6 deletions rust/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use tempfile::tempdir;
use crate::Error;
use crate::metadata::{Discovered, Package};

const RESOLVER_IMPORT_ERROR: &str = "The from-index subcommand requires nab-index and nab-project";
const RESOLVER_IMPORT_ERROR: &str =
"The from-index subcommand requires nab, nab-index and nab-project";
const PYPI_NAME: &str = "pypi";
const PYPI_URL: &str = "https://pypi.org/simple";
const GIT_SCHEMES: [&str; 5] = ["git+https", "git+ssh", "git+http", "git+file", "git+git"];
Expand Down Expand Up @@ -480,7 +481,7 @@ impl<'py> ResolverModules<'py> {
Ok(Self {
multi_index: import("nab_index.multi_index")?,
transport: import("nab_index.urllib3_async_transport")?,
config: import("nab_project.config")?,
config: import("nab.config.model")?,
resolve: import("nab_project.resolve")?,
})
}
Expand Down Expand Up @@ -508,12 +509,12 @@ impl<'py> ResolverModules<'py> {
let tuple = PyTuple::new(py, configs.iter())?;
let kwargs = PyDict::new(py);
kwargs.set_item("indexes", tuple)?;
config = PyModule::import(py, "dataclasses")?
.getattr("replace")?
.call((&config,), Some(&kwargs))?;
config = config.call_method("replace", (), Some(&kwargs))?;
}
let kwargs = PyDict::new(py);
kwargs.set_item("config", config)?;
let targets = self.config.getattr("plan_targets")?.call1((&config,))?;
kwargs.set_item("targets", targets)?;
kwargs.set_item("inputs", config.call_method0("resolve_inputs")?)?;
let transport = self.transport.getattr("Urllib3AsyncTransport")?.call0()?;
self.resolve
.getattr("resolve_for_targets")?
Expand Down
7 changes: 3 additions & 4 deletions rust/tests/public_api/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,15 @@ from pathlib import Path
from unittest.mock import create_autospec

from packaging.version import Version
from nab_project.config import NabProjectConfig, plan_targets
from nab_project.lockfile import TargetLock
from nab_project.resolve import ResolveResult, TargetResult
import nab_project.resolve as resolve_module

def resolved(path, transport, *, config):
indexes = [(index.name, index.url) for index in config.indexes]
def resolved(path, transport, *, targets, inputs):
indexes = [(index.name, index.url) for index in inputs.indexes]
text = path.read_text() + "\n--- indexes ---\n" + repr(indexes)
Path(resolve_module.capture).write_text(text)
target = plan_targets(NabProjectConfig())[0]
target = targets[0]
return ResolveResult(
targets=(target,),
target_results=[
Expand Down
2 changes: 1 addition & 1 deletion rust/tests/public_api/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ fn reports_missing_resolver_module() {
(output.code, output.stderr.as_str()),
(
1,
"The from-index subcommand requires nab-index and nab-project\n",
"The from-index subcommand requires nab, nab-index and nab-project\n",
)
);
}
Expand Down
16 changes: 15 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import tempfile
import webbrowser
from functools import partial
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Final
from unittest.mock import MagicMock, create_autospec

import pytest
Expand Down Expand Up @@ -220,6 +220,20 @@ def test_cli_lock(
assert (entry_point(["from-lock", str(lock)]), "locked==1" in capsys.readouterr().out) == (0, True)


def test_cli_index_empty_project(
entry_point: Callable[[Sequence[str] | None], int | None],
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
project: Final = tmp_path / "pyproject.toml"
project.write_text('[project]\nname = "empty"\nversion = "1"\ndependencies = []\n')

code: Final = entry_point(["from-index", "--pyproject", str(project), "--json"])
captured: Final = capsys.readouterr()

assert (code, json.loads(captured.out), captured.err) == (0, [], "")


def test_module_entrypoint(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
monkeypatch.setattr(sys, "argv", ["pipdeptree", "--version"])

Expand Down
12 changes: 6 additions & 6 deletions tox.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
requires = [ "tox>=4.57.1", "tox-uv>=1.35.2" ]
requires = [ "tox>=4.61.2", "tox-uv>=1.36" ]
env_list = [
"3.15",
"3.14",
Expand Down Expand Up @@ -28,7 +28,7 @@ commands = [
"{tty:--color=yes}",
{ replace = "posargs", default = [
"--cov",
"{env_site_packages_dir}{/}pipdeptree",
"pipdeptree",
"--cov",
"{tox_root}{/}tests",
"--cov-config=pyproject.toml",
Expand Down Expand Up @@ -102,13 +102,13 @@ commands = [
[env.fix]
description = "format the code base and report remaining style violations"
skip_install = true
deps = [ "pre-commit-uv>=4.2.2" ]
deps = [ "pre-commit-uv>=4.3" ]
commands = [ [ "pre-commit", "run", "--all-files", "--show-diff-on-failure" ] ]

[env.pkg_meta]
description = "check that the long description is valid"
skip_install = true
deps = [ "check-wheel-contents>=0.6.3", "twine>=6.2", "uv>=0.11.29" ]
deps = [ "check-wheel-contents>=0.6.3", "twine>=7", "uv>=0.12.10" ]
commands = [
[ "uv", "build", "--sdist", "--wheel", "--out-dir", "{env_tmp_dir}", "." ],
[ "twine", "check", "{env_tmp_dir}{/}*" ],
Expand All @@ -117,7 +117,7 @@ commands = [

[env.type]
description = "run type check on code base"
deps = [ "ty>=0.0.61" ]
deps = [ "ty>=0.0.79" ]
commands = [ [ "ty", "check", "--python-platform", "all", "--output-format", "concise", "--error-on-warning", "." ] ]

[env.dev]
Expand Down Expand Up @@ -161,6 +161,6 @@ commands_pre = [
[env.release]
description = "build the changelog for the next release, then commit and tag it"
skip_install = true
deps = [ "towncrier>=25.8" ]
deps = [ "towncrier>=26.9" ]
pass_env = [ "GH_TOKEN" ]
commands = [ [ "python", "{tox_root}{/}tools{/}release.py", { replace = "posargs", extend = true } ] ]
Loading