Repository: tox-dev/pipdeptree
Pull request #215 (2023-03-24 15:32:54)

Implement: Use fnmatch for include and exclude #213
see proposal in #213 for details


Patch:
diff --git a/README.md b/README.md
index b1e06d2..9d1b3a5 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ works for packages installed globally on a machine as well as in a virtualenv. S
 as a flat list, finding out which are the top level packages and which packages do they depend on requires some effort.
 It\'s also tedious to resolve conflicting dependencies that could have been installed because older version of `pip`
 didn\'t have true dependency resolution[^1]. `pipdeptree` can help here by identifying conflicting dependencies
-installed in the environment.R
+installed in the environment.
 
 To some extent, `pipdeptree` is inspired by the `lein deps :tree` command of [Leiningen](http://leiningen.org/).
 
@@ -27,7 +27,7 @@ pipdeptree has been tested with Python versions `3.7`, `3.8`, `3.9` and `3.10`.
 If you want to run pipdeptree in the context of a particular virtualenv, you can specify the `--python` option. Note
 that this capability has been recently added in version `2.0.0`.
 
-Alternately, you may also install pipdeptree inside the virtualenv and then run it from there.
+Alternatively, you may also install pipdeptree inside the virtualenv and then run it from there.
 
 ## Usage and examples
 
@@ -250,10 +250,12 @@ optional arguments:
                         packages that need them under them.
   -p PACKAGES, --packages PACKAGES
                         Comma separated list of select packages to show in the
-                        output. If set, --all will be ignored.
+                        output. Wildcards are supported, like 'somepackage.*'.
+                        If set, --all will be ignored.
   -e PACKAGES, --exclude PACKAGES
                         Comma separated list of select packages to exclude
-                        from the output. If set, --all will be ignored.
+                        from the output. Wildcards are supported, like
+                        'somepackage.*'. If set, --all will be ignored.
   -j, --json            Display dependency tree as json. This will yield "raw"
                         output that may be used by external tools. This option
                         overrides all other options.
diff --git a/src/pipdeptree/__init__.py b/src/pipdeptree/__init__.py
index 8074ead..c9d6637 100644
--- a/src/pipdeptree/__init__.py
+++ b/src/pipdeptree/__init__.py
@@ -1,4 +1,5 @@
 import argparse
+import fnmatch
 import inspect
 import json
 import os
@@ -350,14 +351,14 @@ class PackageDAG(Mapping):
         m = {}
         seen = set()
         for node in self._obj.keys():
-            if node.key in exclude:
+            if any(fnmatch.fnmatch(node.key, e) for e in exclude):
                 continue
-            if include is None or node.key in include:
+            if include is None or any(fnmatch.fnmatch(node.key, i) for i in include):
                 stack.append(node)
             while True:
                 if len(stack) > 0:
                     n = stack.pop()
-                    cldn = [c for c in self._obj[n] if c.key not in exclude]
+                    cldn = [c for c in self._obj[n] if not any(fnmatch.fnmatch(c.key, e) for e in exclude)]
                     m[n] = cldn
                     seen.add(n.key)
                     for c in cldn:
@@ -844,12 +845,20 @@ def get_parser():
     parser.add_argument(
         "-p",
         "--packages",
-        help="Comma separated list of select packages to show " "in the output. If set, --all will be ignored.",
+        help=(
+            "Comma separated list of select packages to show in the output. "
+            "Wildcards are supported, like 'somepackage.*'. "
+            "If set, --all will be ignored."
+        ),
     )
     parser.add_argument(
         "-e",
         "--exclude",
-        help="Comma separated list of select packages to exclude " "from the output. If set, --all will be ignored.",
+        help=(
+            "Comma separated list of select packages to exclude from the output. "
+            "Wildcards are supported, like 'somepackage.*'. "
+            "If set, --all will be ignored."
+        ),
         metavar="PACKAGES",
     )
     parser.add_argument(
diff --git a/tox.ini b/tox.ini
index 1534d59..e932970 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,7 @@
 [tox]
 envlist =
     fix
+    py311
     py310
     py39
     py38


Test patch:
diff --git a/tests/test_pipdeptree.py b/tests/test_pipdeptree.py
index 5eb1d58..fdba646 100644
--- a/tests/test_pipdeptree.py
+++ b/tests/test_pipdeptree.py
@@ -7,6 +7,7 @@ from itertools import chain
 from pathlib import Path
 from tempfile import NamedTemporaryFile
 from textwrap import dedent, indent
+from typing import Any
 
 try:
     from unittest import mock
@@ -94,6 +95,43 @@ def test_package_dag_filter():
         dag_to_dict(t.filter({"d"}, {"D", "e"}))
 
 
+@pytest.fixture(scope="session")
+def t_fnmatch() -> Any:
+    return mock_package_dag(
+        {
+            ("a.a", "1"): [("a.b", []), ("a.c", [])],
+            ("a.b", "1"): [("a.c", [])],
+            ("b.a", "1"): [("b.b", [])],
+            ("b.b", "1"): [("a.b", [])],
+        }
+    )
+
+
+def test_package_dag_filter_fnmatch_include_a(t_fnmatch: Any) -> None:
+    # test include for a.*in the result we got only a.* nodes
+    graph = dag_to_dict(t_fnmatch.filter({"a.*"}, None))
+    assert graph == {"a.a": ["a.b", "a.c"], "a.b": ["a.c"]}
+
+
+def test_package_dag_filter_fnmatch_include_b(t_fnmatch: Any) -> None:
+    # test include for b.*, which has a.b and a.c in tree, but not a.a
+    # in the result we got the b.* nodes plus the a.b node as child in the tree
+    graph = dag_to_dict(t_fnmatch.filter({"b.*"}, None))
+    assert graph == {"b.a": ["b.b"], "b.b": ["a.b"], "a.b": ["a.c"]}
+
+
+def test_package_dag_filter_fnmatch_exclude_c(t_fnmatch: Any) -> None:
+    # test exclude for b.* in the result we got only a.* nodes
+    graph = dag_to_dict(t_fnmatch.filter(None, {"b.*"}))
+    assert graph == {"a.a": ["a.b", "a.c"], "a.b": ["a.c"]}
+
+
+def test_package_dag_filter_fnmatch_exclude_a(t_fnmatch: Any) -> None:
+    # test exclude for a.* in the result we got only b.* nodes
+    graph = dag_to_dict(t_fnmatch.filter(None, {"a.*"}))
+    assert graph == {"b.a": ["b.b"], "b.b": []}
+
+
 def test_package_dag_reverse():
     t1 = t.reverse()
     expected = {"a": [], "b": ["a", "f"], "c": ["a"], "d": ["b", "c"], "e": ["c", "d", "g"], "f": ["g"], "g": []}