From 3d3c80577746431afee6b7ee3d83c9a9ac55fa79 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Sat, 25 Mar 2023 21:24:21 +0100 Subject: [PATCH] Implement: Use fnmatch for include and exclude #213 (#215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature: fnmatch based includes and excludes This is to support wildcards. * PR Feedback Signed-off-by: Bernát Gábor --------- Signed-off-by: Bernát Gábor Co-authored-by: Bernát Gábor --- README.md | 10 ++++++---- src/pipdeptree/__init__.py | 19 ++++++++++++++----- tests/test_pipdeptree.py | 38 ++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 4 files changed, 59 insertions(+), 9 deletions(-) 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/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": []} 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