diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 038412d..565a442 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: - id: prettier args: ["--print-width=120", "--prose-wrap=always"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.0.278" + rev: "v0.0.280" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/src/pipdeptree/_cli.py b/src/pipdeptree/_cli.py index a997aeb..1224902 100644 --- a/src/pipdeptree/_cli.py +++ b/src/pipdeptree/_cli.py @@ -61,6 +61,21 @@ def build_parser() -> ArgumentParser: select = parser.add_argument_group(title="select", description="choose what to render") select.add_argument("--python", default=sys.executable, help="Python interpreter to inspect") + select.add_argument( + "-p", + "--packages", + help="comma separated list of packages to show - wildcards are supported, like 'somepackage.*'", + metavar="P", + ) + select.add_argument( + "-e", + "--exclude", + help="comma separated list of packages to not show - wildcards are supported, like 'somepackage.*'. " + "(cannot combine with -p or -a)", + metavar="P", + ) + select.add_argument("-a", "--all", action="store_true", help="list all deps at top level") + scope = select.add_mutually_exclusive_group() scope.add_argument( "-l", @@ -70,21 +85,6 @@ def build_parser() -> ArgumentParser: ) scope.add_argument("-u", "--user-only", action="store_true", help="only show installations in the user site dir") - package = select.add_mutually_exclusive_group() - package.add_argument( - "-p", - "--packages", - help="comma separated list of packages to show - wildcards are supported, like 'somepackage.*'", - metavar="P", - ) - package.add_argument( - "-e", - "--exclude", - help="comma separated list of packages to not show - wildcards are supported, like 'somepackage.*'", - metavar="P", - ) - package.add_argument("-a", "--all", action="store_true", help="list all deps at top level") - render = parser.add_argument_group( title="render", description="choose how to render the dependency tree (by default will use text mode)", @@ -137,7 +137,12 @@ def build_parser() -> ArgumentParser: def get_options(args: Sequence[str] | None) -> Options: parser = build_parser() - return cast(Options, parser.parse_args(args)) + parsed_args = parser.parse_args(args) + + if parsed_args.exclude and (parsed_args.all or parsed_args.packages): + return parser.error("cannot use --exclude with --packages or --all") + + return cast(Options, parsed_args) __all__ = [ diff --git a/tests/render/test_text.py b/tests/render/test_text.py index 5accf0b..e136bbe 100644 --- a/tests/render/test_text.py +++ b/tests/render/test_text.py @@ -1,13 +1,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, Iterator import pytest +from pipdeptree._models import PackageDAG from pipdeptree._render.text import render_text if TYPE_CHECKING: - from pipdeptree._models import PackageDAG + from unittest.mock import Mock + + from tests.our_types import MockGraph @pytest.mark.parametrize( @@ -506,3 +509,32 @@ def test_render_text_encoding( render_text(example_dag, max_depth=level, encoding=encoding, list_all=True, frozen=False) captured = capsys.readouterr() assert "\n".join(expected_output).strip() == captured.out.strip() + + +def test_render_text_list_all_and_packages_options_used( + capsys: pytest.CaptureFixture[str], + mock_pkgs: Callable[[MockGraph], Iterator[Mock]], +) -> None: + graph: dict[tuple[str, str], list[tuple[str, list[tuple[str, str]]]]] = { + ("examplePy", "1.2.3"): [("hellopy", [(">=", "2.0.0")]), ("worldpy", [(">=", "0.0.2")])], + ("HelloPy", "2.0.0"): [], + ("worldpy", "0.0.2"): [], + ("anotherpy", "0.1.2"): [("hellopy", [(">=", "2.0.0")])], + ("YetAnotherPy", "3.1.2"): [], + } + package_dag = PackageDAG.from_pkgs(list(mock_pkgs(graph))) + + # NOTE: Mimicking the --packages option being used here. + package_dag = package_dag.filter_nodes({"examplePy"}, None) + + render_text(package_dag, max_depth=float("inf"), encoding="utf-8", list_all=True, frozen=False) + captured = capsys.readouterr() + expected_output = [ + "examplePy==1.2.3", + "├── HelloPy [required: >=2.0.0, installed: 2.0.0]", + "└── worldpy [required: >=0.0.2, installed: 0.0.2]", + "HelloPy==2.0.0", + "worldpy==0.0.2", + ] + + assert "\n".join(expected_output).strip() == captured.out.strip() diff --git a/tests/test_cli.py b/tests/test_cli.py index 84d6271..75a26d3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,7 +2,7 @@ from __future__ import annotations import pytest -from pipdeptree._cli import build_parser +from pipdeptree._cli import build_parser, get_options def test_parser_default() -> None: @@ -75,3 +75,25 @@ def test_parser_depth(should_be_error: bool, depth_arg: list[str], expected_valu else: args = parser.parse_args(depth_arg) assert args.depth == expected_value + + +@pytest.mark.parametrize( + "args", + [ + pytest.param(["--exclude", "py", "--all"], id="exclude-all"), + pytest.param(["-e", "py", "--packages", "py2"], id="exclude-packages"), + pytest.param(["-e", "py", "-p", "py2", "-a"], id="exclude-packages-all"), + ], +) +def test_parser_get_options_exclude_combine_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 --exclude with --packages or --all" in err + + +def test_parser_get_options_exclude_only() -> None: + parsed_args = get_options(["--exclude", "py"]) + assert parsed_args.exclude == "py"