Skip to content

Commit 75ac1c6

Browse files
authored
Allow --exclude to be used with --include (#491)
#273 removed the ability to use --include and --exclude together. I now disagree with this change since not only was that a (accidental) breaking change but I now see it makes sense to allow them to be used together (e.g using pattern matching: `pidpeptree -p pytest* -e pytest-mock`). This change allows them to be used together and adds a few tests to ensure that they continue to do so.
1 parent 8ade104 commit 75ac1c6

File tree

6 files changed

+44
-37
lines changed

6 files changed

+44
-37
lines changed

src/pipdeptree/__main__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pipdeptree._detect_env import detect_active_interpreter
1010
from pipdeptree._discovery import InterpreterQueryError, get_installed_distributions
1111
from pipdeptree._models import PackageDAG
12+
from pipdeptree._models.dag import IncludeExcludeOverlapError, IncludePatternNotFoundError
1213
from pipdeptree._render import render
1314
from pipdeptree._validate import validate
1415
from pipdeptree._warning import WarningPrinter, WarningType, get_warning_printer
@@ -52,13 +53,16 @@ def main(args: Sequence[str] | None = None) -> int | None:
5253
if options.reverse:
5354
tree = tree.reverse()
5455

55-
show_only = options.packages.split(",") if options.packages else None
56+
include = options.packages.split(",") if options.packages else None
5657
exclude = set(options.exclude.split(",")) if options.exclude else None
5758

58-
if show_only is not None or exclude is not None:
59+
if include is not None or exclude is not None:
5960
try:
60-
tree = tree.filter_nodes(show_only, exclude, exclude_deps=options.exclude_dependencies)
61-
except ValueError as e:
61+
tree = tree.filter_nodes(include, exclude, exclude_deps=options.exclude_dependencies)
62+
except IncludeExcludeOverlapError:
63+
print("Cannot have --packages and --exclude contain the same entries", file=sys.stderr) # noqa: T201
64+
return 1
65+
except IncludePatternNotFoundError as e:
6266
if warning_printer.should_warn():
6367
warning_printer.print_single_line(str(e))
6468
return _determine_return_code(warning_printer)

src/pipdeptree/_cli.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,6 @@ def get_options(args: Sequence[str] | None) -> Options:
170170
parser = build_parser()
171171
parsed_args = parser.parse_args(args)
172172

173-
if parsed_args.exclude and (parsed_args.all or parsed_args.packages):
174-
return parser.error("cannot use --exclude with --packages or --all")
175173
if parsed_args.exclude_dependencies and not parsed_args.exclude:
176174
return parser.error("must use --exclude-dependencies with --exclude")
177175
if parsed_args.license and parsed_args.freeze:

src/pipdeptree/_models/dag.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818
from .package import DistPackage, InvalidRequirementError, ReqPackage
1919

2020

21+
class IncludeExcludeOverlapError(Exception):
22+
"""Include and exclude sets passed as input violate mutual exclusivity requirement."""
23+
24+
25+
class IncludePatternNotFoundError(Exception):
26+
"""Include patterns weren't found when filtering a `PackageDAG`."""
27+
28+
2129
def render_invalid_reqs_text(dist_name_to_invalid_reqs_dict: dict[str, list[str]]) -> None:
2230
for dist_name, invalid_reqs in dist_name_to_invalid_reqs_dict.items():
2331
print(dist_name, file=sys.stderr) # noqa: T201
@@ -139,7 +147,8 @@ def filter_nodes( # noqa: C901, PLR0912
139147
140148
:param include: list of node keys to include (or None)
141149
:param exclude: set of node keys to exclude (or None)
142-
:raises ValueError: If include has node keys that do not exist in the graph
150+
:raises IncludeExcludeOverlapError: if include and exclude contains the same elements
151+
:raises IncludePatternNotFoundError: if include has patterns that do not match anything in the graph
143152
:returns: filtered version of the graph
144153
145154
"""
@@ -153,10 +162,10 @@ def filter_nodes( # noqa: C901, PLR0912
153162
include = [canonicalize_name(i) for i in include]
154163
exclude = {canonicalize_name(s) for s in exclude} if exclude else set()
155164

156-
# Check for mutual exclusion of show_only and exclude sets
165+
# Check for mutual exclusion of include and exclude sets
157166
# after normalizing the values to lowercase
158-
if include and exclude:
159-
assert not (set(include) & exclude)
167+
if include and exclude and (set(include) & exclude):
168+
raise IncludeExcludeOverlapError
160169

161170
if exclude_deps:
162171
exclude = self._build_exclusion_set_with_dependencies(exclude)
@@ -203,7 +212,9 @@ def filter_nodes( # noqa: C901, PLR0912
203212
i for i in include_with_casing_preserved if canonicalize_name(i) not in matched_includes
204213
]
205214
if non_existent_includes:
206-
raise ValueError("No packages matched using the following patterns: " + ", ".join(non_existent_includes))
215+
raise IncludePatternNotFoundError(
216+
"No packages matched using the following patterns: " + ", ".join(non_existent_includes)
217+
)
207218

208219
return self.__class__(m)
209220

tests/_models/test_dag.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77

88
from pipdeptree._models import DistPackage, PackageDAG, ReqPackage, ReversedPackageDAG
9+
from pipdeptree._models.dag import IncludeExcludeOverlapError, IncludePatternNotFoundError
910

1011
if TYPE_CHECKING:
1112
from collections.abc import Iterator
@@ -63,13 +64,18 @@ def test_package_dag_filter_fnmatch_exclude_a(t_fnmatch: PackageDAG) -> None:
6364
assert graph == {"b-a": ["b-b"], "b-b": []}
6465

6566

66-
def test_package_dag_filter_include_exclude_both_used(t_fnmatch: PackageDAG) -> None:
67-
with pytest.raises(AssertionError):
67+
def test_package_dag_filter_include_exclude_normal(t_fnmatch: PackageDAG) -> None:
68+
graph = dag_to_dict(t_fnmatch.filter_nodes(["a-*"], {"a-a"}))
69+
assert graph == {"a-b": ["a-c"]}
70+
71+
72+
def test_package_dag_filter_include_exclude_overlap(t_fnmatch: PackageDAG) -> None:
73+
with pytest.raises(IncludeExcludeOverlapError):
6874
t_fnmatch.filter_nodes(["a-a", "a-b"], {"a-b"})
6975

7076

71-
def test_package_dag_filter_nonexistent_packages(t_fnmatch: PackageDAG) -> None:
72-
with pytest.raises(ValueError, match="No packages matched using the following patterns: x, y, z"):
77+
def test_package_dag_filter_include_nonexistent_packages(t_fnmatch: PackageDAG) -> None:
78+
with pytest.raises(IncludePatternNotFoundError, match="No packages matched using the following patterns: x, y, z"):
7379
t_fnmatch.filter_nodes(["x", "y", "z"], None)
7480

7581

tests/test_cli.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,28 +81,6 @@ def test_parser_depth(should_be_error: bool, depth_arg: list[str], expected_valu
8181
assert args.depth == expected_value
8282

8383

84-
@pytest.mark.parametrize(
85-
"args",
86-
[
87-
pytest.param(["--exclude", "py", "--all"], id="exclude-all"),
88-
pytest.param(["-e", "py", "--packages", "py2"], id="exclude-packages"),
89-
pytest.param(["-e", "py", "-p", "py2", "-a"], id="exclude-packages-all"),
90-
],
91-
)
92-
def test_parser_get_options_exclude_combine_not_supported(args: list[str], capsys: pytest.CaptureFixture[str]) -> None:
93-
with pytest.raises(SystemExit, match="2"):
94-
get_options(args)
95-
96-
out, err = capsys.readouterr()
97-
assert not out
98-
assert "cannot use --exclude with --packages or --all" in err
99-
100-
101-
def test_parser_get_options_exclude_only() -> None:
102-
parsed_args = get_options(["--exclude", "py"])
103-
assert parsed_args.exclude == "py"
104-
105-
10684
def test_parser_get_options_license_and_freeze_together_not_supported(capsys: pytest.CaptureFixture[str]) -> None:
10785
with pytest.raises(SystemExit, match="2"):
10886
get_options(["--license", "--freeze"])

tests/test_pipdeptree.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,13 @@ def test_main_log_resolved(tmp_path: Path, mocker: MockFixture, capsys: pytest.C
5858

5959
captured = capsys.readouterr()
6060
assert captured.err.startswith(f"(resolved python: {tmp_path!s}")
61+
62+
63+
def test_main_include_and_exclude_overlap(mocker: MockFixture, capsys: pytest.CaptureFixture[str]) -> None:
64+
cmd = ["", "--packages", "a,b,c", "--exclude", "a"]
65+
mocker.patch("pipdeptree.__main__.sys.argv", cmd)
66+
67+
main()
68+
69+
captured = capsys.readouterr()
70+
assert "Cannot have --packages and --exclude contain the same entries" in captured.err

0 commit comments

Comments
 (0)