Skip to content

Commit c31b641

Browse files
kemzebgaborbernat
andauthored
Implement --path argument (#429)
Resolves #408. --------- Co-authored-by: Bernát Gábor <[email protected]>
1 parent 139677b commit c31b641

File tree

6 files changed

+62
-8
lines changed

6 files changed

+62
-8
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ select:
240240
choose what to render
241241
242242
--python PYTHON Python interpreter to inspect (default: /usr/local/bin/python)
243+
--path PATH Passes a path used to restrict where packages should be looked for (can be used multiple times) (default: None)
243244
-p P, --packages P comma separated list of packages to show - wildcards are supported, like 'somepackage.*' (default: None)
244245
-e P, --exclude P comma separated list of packages to not show - wildcards are supported, like 'somepackage.*'. (cannot combine with -p or -a) (default: None)
245246
-a, --all list all deps at top level (default: False)

src/pipdeptree/__main__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ def main(args: Sequence[str] | None = None) -> int | None:
3131
print(f"(resolved python: {resolved_path})", file=sys.stderr) # noqa: T201
3232

3333
pkgs = get_installed_distributions(
34-
interpreter=options.python, local_only=options.local_only, user_only=options.user_only
34+
interpreter=options.python,
35+
supplied_paths=options.path or None,
36+
local_only=options.local_only,
37+
user_only=options.user_only,
3538
)
3639
tree = PackageDAG.from_pkgs(pkgs)
3740

src/pipdeptree/_cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
class Options(Namespace):
1414
freeze: bool
1515
python: str
16+
path: list[str]
1617
all: bool
1718
local_only: bool
1819
user_only: bool
@@ -60,6 +61,11 @@ def build_parser() -> ArgumentParser:
6061
" it can't."
6162
),
6263
)
64+
select.add_argument(
65+
"--path",
66+
help="Passes a path used to restrict where packages should be looked for (can be used multiple times)",
67+
action="append",
68+
)
6369
select.add_argument(
6470
"-p",
6571
"--packages",
@@ -157,6 +163,8 @@ def get_options(args: Sequence[str] | None) -> Options:
157163
return parser.error("cannot use --exclude with --packages or --all")
158164
if parsed_args.license and parsed_args.freeze:
159165
return parser.error("cannot use --license with --freeze")
166+
if parsed_args.path and (parsed_args.local_only or parsed_args.user_only):
167+
return parser.error("cannot use --path with --user-only or --local-only")
160168

161169
return cast(Options, parsed_args)
162170

src/pipdeptree/_discovery.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@
1515

1616
def get_installed_distributions(
1717
interpreter: str = str(sys.executable),
18+
supplied_paths: list[str] | None = None,
1819
local_only: bool = False, # noqa: FBT001, FBT002
1920
user_only: bool = False, # noqa: FBT001, FBT002
2021
) -> list[Distribution]:
21-
# We assign sys.path here as it used by both importlib.metadata.PathDistribution and pip by default.
22-
paths = sys.path
22+
# This will be the default since it's used by both importlib.metadata.PathDistribution and pip by default.
23+
computed_paths = supplied_paths or sys.path
2324

2425
# See https://docs.python.org/3/library/venv.html#how-venvs-work for more details.
2526
in_venv = sys.prefix != sys.base_prefix
2627

2728
py_path = Path(interpreter).absolute()
2829
using_custom_interpreter = py_path != Path(sys.executable).absolute()
30+
should_query_interpreter = using_custom_interpreter and not supplied_paths
2931

30-
if using_custom_interpreter:
32+
if should_query_interpreter:
3133
# We query the interpreter directly to get its `sys.path`. If both --python and --local-only are given, only
3234
# snatch metadata associated to the interpreter's environment.
3335
if local_only:
@@ -37,14 +39,14 @@ def get_installed_distributions(
3739

3840
args = [str(py_path), "-c", cmd]
3941
result = subprocess.run(args, stdout=subprocess.PIPE, check=False, text=True) # noqa: S603
40-
paths = ast.literal_eval(result.stdout)
42+
computed_paths = ast.literal_eval(result.stdout)
4143
elif local_only and in_venv:
42-
paths = [p for p in paths if p.startswith(sys.prefix)]
44+
computed_paths = [p for p in computed_paths if p.startswith(sys.prefix)]
4345

4446
if user_only:
45-
paths = [p for p in paths if p.startswith(site.getusersitepackages())]
47+
computed_paths = [p for p in computed_paths if p.startswith(site.getusersitepackages())]
4648

47-
return filter_valid_distributions(distributions(path=paths))
49+
return filter_valid_distributions(distributions(path=computed_paths))
4850

4951

5052
def filter_valid_distributions(iterable_dists: Iterable[Distribution]) -> list[Distribution]:

tests/test_cli.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ def test_parser_get_options_license_and_freeze_together_not_supported(capsys: py
112112
assert "cannot use --license with --freeze" in err
113113

114114

115+
@pytest.mark.parametrize(
116+
"args",
117+
[
118+
pytest.param(["--path", "/random/path", "--local-only"], id="path-with-local"),
119+
pytest.param(["--path", "/random/path", "--user-only"], id="path-with-user"),
120+
],
121+
)
122+
def test_parser_get_options_path_with_either_local_or_user_not_supported(
123+
args: list[str], capsys: pytest.CaptureFixture[str]
124+
) -> None:
125+
with pytest.raises(SystemExit, match="2"):
126+
get_options(args)
127+
128+
out, err = capsys.readouterr()
129+
assert not out
130+
assert "cannot use --path with --user-only or --local-only" in err
131+
132+
115133
@pytest.mark.parametrize(("bad_type"), [None, str])
116134
def test_enum_action_type_argument(bad_type: Any) -> None:
117135
with pytest.raises(TypeError, match="type must be a subclass of Enum"):

tests/test_discovery.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,25 @@ def test_invalid_metadata(
162162
f"{fake_site_dir}\n"
163163
"------------------------------------------------------------------------\n"
164164
)
165+
166+
167+
def test_paths(fake_dist: Path) -> None:
168+
fake_site_dir = str(fake_dist.parent)
169+
mocked_path = [fake_site_dir]
170+
171+
dists = get_installed_distributions(supplied_paths=mocked_path)
172+
assert len(dists) == 1
173+
assert dists[0].name == "bar"
174+
175+
176+
def test_paths_when_in_virtual_env(tmp_path: Path, fake_dist: Path) -> None:
177+
# tests to ensure that we use only the user-supplied path, not paths in the virtual env
178+
fake_site_dir = str(fake_dist.parent)
179+
mocked_path = [fake_site_dir]
180+
181+
venv_path = str(tmp_path / "venv")
182+
s = virtualenv.cli_run([venv_path, "--activators", ""])
183+
184+
dists = get_installed_distributions(interpreter=str(s.creator.exe), supplied_paths=mocked_path)
185+
assert len(dists) == 1
186+
assert dists[0].name == "bar"

0 commit comments

Comments
 (0)