Skip to content

Commit 25cbb6f

Browse files
authored
refactor the non_host part for not injecting to custom env (#346)
fix: #343 We can get the sitepackages directory from the given interpreter directly instead of running our code again with it.
1 parent 268dee8 commit 25cbb6f

File tree

6 files changed

+69
-86
lines changed

6 files changed

+69
-86
lines changed

src/pipdeptree/__main__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,17 @@
88
from pipdeptree._cli import get_options
99
from pipdeptree._discovery import get_installed_distributions
1010
from pipdeptree._models import PackageDAG
11-
from pipdeptree._non_host import handle_non_host_target
1211
from pipdeptree._render import render
1312
from pipdeptree._validate import validate
1413

1514

1615
def main(args: Sequence[str] | None = None) -> None | int:
1716
"""CLI - The main function called as entry point."""
1817
options = get_options(args)
19-
result = handle_non_host_target(options)
20-
if result is not None:
21-
return result
2218

23-
pkgs = get_installed_distributions(local_only=options.local_only, user_only=options.user_only)
19+
pkgs = get_installed_distributions(
20+
interpreter=options.python, local_only=options.local_only, user_only=options.user_only
21+
)
2422
tree = PackageDAG.from_pkgs(pkgs)
2523
is_text_output = not any([options.json, options.json_tree, options.output_format])
2624

src/pipdeptree/_discovery.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,44 @@
11
from __future__ import annotations
22

3+
import ast
34
import site
5+
import subprocess # noqa: S404
46
import sys
57
from importlib.metadata import Distribution, distributions
8+
from pathlib import Path
69
from typing import Iterable, Tuple
710

811
from packaging.utils import canonicalize_name
912

1013

1114
def get_installed_distributions(
15+
interpreter: str = str(sys.executable),
1216
local_only: bool = False, # noqa: FBT001, FBT002
1317
user_only: bool = False, # noqa: FBT001, FBT002
1418
) -> list[Distribution]:
1519
# See https://docs.python.org/3/library/venv.html#how-venvs-work for more details.
1620
in_venv = sys.prefix != sys.base_prefix
1721
original_dists: Iterable[Distribution] = []
22+
py_path = Path(interpreter).absolute()
23+
using_custom_interpreter = py_path != Path(sys.executable).absolute()
1824

19-
if local_only and in_venv:
20-
venv_site_packages = site.getsitepackages([sys.prefix])
21-
original_dists = distributions(path=venv_site_packages)
22-
elif user_only:
25+
if user_only:
2326
original_dists = distributions(path=[site.getusersitepackages()])
27+
elif using_custom_interpreter:
28+
# We query the interpreter directly to get its `sys.path` list to be used by `distributions()`.
29+
# If --python and --local-only are given, we ensure that we are only using paths associated to the interpreter's
30+
# environment.
31+
if local_only:
32+
cmd = "import sys; print([p for p in sys.path if p.startswith(sys.prefix)])"
33+
else:
34+
cmd = "import sys; print(sys.path)"
35+
36+
args = [str(py_path), "-c", cmd]
37+
result = subprocess.run(args, stdout=subprocess.PIPE, check=False, text=True) # noqa: S603
38+
original_dists = distributions(path=ast.literal_eval(result.stdout))
39+
elif local_only and in_venv:
40+
venv_site_packages = [p for p in sys.path if p.startswith(sys.prefix)]
41+
original_dists = distributions(path=venv_site_packages)
2442
else:
2543
original_dists = distributions()
2644

src/pipdeptree/_non_host.py

Lines changed: 0 additions & 61 deletions
This file was deleted.

tests/_models/test_package.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import sys
34
from importlib.metadata import PackageNotFoundError
45
from pathlib import Path
56
from typing import TYPE_CHECKING, Any
@@ -26,11 +27,13 @@ def test_guess_version_setuptools(mocker: MockerFixture) -> None:
2627
assert result == "?"
2728

2829

29-
def test_package_as_frozen_repr(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
30+
def test_package_as_frozen_repr(tmp_path: Path, mocker: MockerFixture) -> None:
3031
file_path = tmp_path / "foo.egg-link"
3132
with Path(file_path).open("w") as f:
3233
f.write("/A/B/foo")
33-
monkeypatch.syspath_prepend(str(tmp_path))
34+
mock_path = sys.path.copy()
35+
mock_path.append(str(tmp_path))
36+
mocker.patch("pipdeptree._discovery.sys.path", mock_path)
3437
json_text = '{"dir_info": {"editable": true}}'
3538
foo = Mock(metadata={"Name": "foo"}, version="20.4.1")
3639
foo.read_text = Mock(return_value=json_text)

tests/test_discovery.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pytest_mock import MockerFixture
1717

1818

19-
def test_local_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capfd: pytest.CaptureFixture[str]) -> None:
19+
def test_local_only(tmp_path: Path, mocker: MockerFixture, capfd: pytest.CaptureFixture[str]) -> None:
2020
venv_path = str(tmp_path / "venv")
2121
result = virtualenv.cli_run([venv_path, "--activators", ""])
2222
venv_site_packages = site.getsitepackages([venv_path])
@@ -27,8 +27,11 @@ def test_local_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capfd: pyte
2727
f.write("Metadata-Version: 2.3\n" "Name: foo\n" "Version: 1.2.5\n")
2828

2929
cmd = [str(result.creator.exe.parent / "python3"), "--local-only"]
30-
monkeypatch.setattr(sys, "prefix", venv_path)
31-
monkeypatch.setattr(sys, "argv", cmd)
30+
mocker.patch("pipdeptree._discovery.sys.prefix", venv_path)
31+
sys_path = sys.path.copy()
32+
mock_path = sys_path + venv_site_packages
33+
mocker.patch("pipdeptree._discovery.sys.path", mock_path)
34+
mocker.patch("pipdeptree._discovery.sys.argv", cmd)
3235
main()
3336
out, _ = capfd.readouterr()
3437
found = {i.split("==")[0] for i in out.splitlines()}
@@ -39,16 +42,16 @@ def test_local_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capfd: pyte
3942
assert found == expected
4043

4144

42-
def test_user_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capfd: pytest.CaptureFixture[str]) -> None:
45+
def test_user_only(tmp_path: Path, mocker: MockerFixture, capfd: pytest.CaptureFixture[str]) -> None:
4346
fake_dist = Path(tmp_path) / "foo-1.2.5.dist-info"
4447
fake_dist.mkdir()
4548
fake_metadata = Path(fake_dist) / "METADATA"
4649
with Path(fake_metadata).open("w") as f:
4750
f.write("Metadata-Version: 2.3\n" "Name: foo\n" "Version: 1.2.5\n")
4851

49-
monkeypatch.setattr(site, "getusersitepackages", Mock(return_value=str(tmp_path)))
5052
cmd = [sys.executable, "--user-only"]
51-
monkeypatch.setattr(sys, "argv", cmd)
53+
mocker.patch("pipdeptree._discovery.site.getusersitepackages", Mock(return_value=str(tmp_path)))
54+
mocker.patch("pipdeptree._discovery.sys.argv", cmd)
5255
main()
5356
out, _ = capfd.readouterr()
5457
found = {i.split("==")[0] for i in out.splitlines()}

tests/test_non_host.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
if TYPE_CHECKING:
1313
from pathlib import Path
1414

15+
from pytest_mock import MockerFixture
16+
1517

1618
@pytest.mark.parametrize("args_joined", [True, False])
1719
def test_custom_interpreter(
1820
tmp_path: Path,
21+
mocker: MockerFixture,
1922
monkeypatch: pytest.MonkeyPatch,
2023
capfd: pytest.CaptureFixture[str],
2124
args_joined: bool,
@@ -25,7 +28,7 @@ def test_custom_interpreter(
2528
monkeypatch.chdir(tmp_path)
2629
py = str(result.creator.exe.relative_to(tmp_path))
2730
cmd += [f"--python={result.creator.exe}"] if args_joined else ["--python", py]
28-
monkeypatch.setattr(sys, "argv", cmd)
31+
mocker.patch("pipdeptree._discovery.sys.argv", cmd)
2932
main()
3033
out, _ = capfd.readouterr()
3134
found = {i.split("==")[0] for i in out.splitlines()}
@@ -40,10 +43,29 @@ def test_custom_interpreter(
4043
expected -= {"setuptools", "wheel"}
4144
assert found == expected, out
4245

43-
monkeypatch.setattr(sys, "argv", [*cmd, "--graph-output", "something"])
44-
with pytest.raises(SystemExit) as context:
45-
main()
46-
out, err = capfd.readouterr()
47-
assert context.value.code == 1
48-
assert not out
49-
assert err == "graphviz functionality is not supported when querying non-host python\n"
46+
47+
def test_custom_interpreter_with_local_only(
48+
tmp_path: Path,
49+
mocker: MockerFixture,
50+
capfd: pytest.CaptureFixture[str],
51+
) -> None:
52+
venv_path = str(tmp_path / "venv")
53+
54+
result = virtualenv.cli_run([venv_path, "--system-site-packages", "--activators", ""])
55+
56+
cmd = ["", f"--python={result.creator.exe}", "--local-only"]
57+
mocker.patch("pipdeptree._discovery.sys.prefix", venv_path)
58+
mocker.patch("pipdeptree._discovery.sys.argv", cmd)
59+
main()
60+
out, _ = capfd.readouterr()
61+
found = {i.split("==")[0] for i in out.splitlines()}
62+
implementation = python_implementation()
63+
if implementation == "CPython":
64+
expected = {"pip", "setuptools", "wheel"}
65+
elif implementation == "PyPy": # pragma: no cover
66+
expected = {"cffi", "greenlet", "pip", "readline", "setuptools", "wheel"} # pragma: no cover
67+
else:
68+
raise ValueError(implementation) # pragma: no cover
69+
if sys.version_info >= (3, 12):
70+
expected -= {"setuptools", "wheel"} # pragma: no cover
71+
assert found == expected, out

0 commit comments

Comments
 (0)