Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement --path argument #429

Merged
merged 3 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 4 additions & 1 deletion src/pipdeptree/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ def main(args: Sequence[str] | None = None) -> int | None:
print(f"(resolved python: {resolved_path})", file=sys.stderr) # noqa: T201

pkgs = get_installed_distributions(
interpreter=options.python, local_only=options.local_only, user_only=options.user_only
interpreter=options.python,
supplied_paths=options.path or None,
local_only=options.local_only,
user_only=options.user_only,
)
tree = PackageDAG.from_pkgs(pkgs)

Expand Down
8 changes: 8 additions & 0 deletions src/pipdeptree/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
class Options(Namespace):
freeze: bool
python: str
path: list[str]
all: bool
local_only: bool
user_only: bool
Expand Down Expand Up @@ -60,6 +61,11 @@ def build_parser() -> ArgumentParser:
" it can't."
),
)
select.add_argument(
"--path",
help="comma separated list of paths used to restrict where packages should be looked for",
action="append",
kemzeb marked this conversation as resolved.
Show resolved Hide resolved
)
select.add_argument(
"-p",
"--packages",
Expand Down Expand Up @@ -157,6 +163,8 @@ def get_options(args: Sequence[str] | None) -> Options:
return parser.error("cannot use --exclude with --packages or --all")
if parsed_args.license and parsed_args.freeze:
return parser.error("cannot use --license with --freeze")
if parsed_args.path and (parsed_args.local_only or parsed_args.user_only):
return parser.error("cannot use --path with --user-only or --local-only")

return cast(Options, parsed_args)

Expand Down
16 changes: 9 additions & 7 deletions src/pipdeptree/_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@

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

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

py_path = Path(interpreter).absolute()
using_custom_interpreter = py_path != Path(sys.executable).absolute()
should_query_interpreter = using_custom_interpreter and not supplied_paths

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

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

if user_only:
paths = [p for p in paths if p.startswith(site.getusersitepackages())]
computed_paths = [p for p in computed_paths if p.startswith(site.getusersitepackages())]

return filter_valid_distributions(distributions(path=paths))
return filter_valid_distributions(distributions(path=computed_paths))


def filter_valid_distributions(iterable_dists: Iterable[Distribution]) -> list[Distribution]:
Expand Down
18 changes: 18 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,24 @@ def test_parser_get_options_license_and_freeze_together_not_supported(capsys: py
assert "cannot use --license with --freeze" in err


@pytest.mark.parametrize(
"args",
[
pytest.param(["--path", "/random/path", "--local-only"], id="path-with-local"),
pytest.param(["--path", "/random/path", "--user-only"], id="path-with-user"),
],
)
def test_parser_get_options_path_with_either_local_or_user_not_supported(
args: list[str], capsys: pytest.CaptureFixture[str]
) -> None:
with pytest.raises(SystemExit, match="2"):
get_options(args)

out, err = capsys.readouterr()
assert not out
assert "cannot use --path with --user-only or --local-only" in err


@pytest.mark.parametrize(("bad_type"), [None, str])
def test_enum_action_type_argument(bad_type: Any) -> None:
with pytest.raises(TypeError, match="type must be a subclass of Enum"):
Expand Down
22 changes: 22 additions & 0 deletions tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,25 @@ def test_invalid_metadata(
f"{fake_site_dir}\n"
"------------------------------------------------------------------------\n"
)


def test_paths(fake_dist: Path) -> None:
fake_site_dir = str(fake_dist.parent)
mocked_path = [fake_site_dir]

dists = get_installed_distributions(supplied_paths=mocked_path)
assert len(dists) == 1
assert dists[0].name == "bar"


def test_paths_when_in_virtual_env(tmp_path: Path, fake_dist: Path) -> None:
# tests to ensure that we use only the user-supplied path, not paths in the virtual env
fake_site_dir = str(fake_dist.parent)
mocked_path = [fake_site_dir]

venv_path = str(tmp_path / "venv")
s = virtualenv.cli_run([venv_path, "--activators", ""])

dists = get_installed_distributions(interpreter=str(s.creator.exe), supplied_paths=mocked_path)
assert len(dists) == 1
assert dists[0].name == "bar"