diff --git a/README.rst b/README.rst index 8657020..1d6c190 100644 --- a/README.rst +++ b/README.rst @@ -263,6 +263,9 @@ Usage -p PACKAGES, --packages PACKAGES Comma separated list of select packages to show in the output. 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. -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/pipdeptree.py b/pipdeptree.py index 7283281..4d09141 100644 --- a/pipdeptree.py +++ b/pipdeptree.py @@ -280,8 +280,8 @@ class ReqPackage(Package): 'required_version': self.version_spec} -def render_tree(tree, list_all=True, show_only=None, frozen=False): - """Convert to tree to string representation +def render_tree(tree, list_all=True, show_only=None, frozen=False, exclude=None): + """Convert tree to string representation :param dict tree: the package tree :param bool list_all: whether to list all the pgks at the root @@ -291,6 +291,8 @@ def render_tree(tree, list_all=True, show_only=None, frozen=False): output. This is optional arg, default: None. :param bool frozen: whether or not show the names of the pkgs in the output that's favourable to pip --freeze + :param set exclude: set of select packages to be excluded from the + output. This is optional arg, default: None. :returns: string representation of the tree :rtype: str @@ -310,6 +312,8 @@ def render_tree(tree, list_all=True, show_only=None, frozen=False): nodes = [p for p in nodes if p.key not in branch_keys] def aux(node, parent=None, indent=0, chain=None): + if exclude and (node.key in exclude or node.project_name in exclude): + return [] if chain is None: chain = [node.project_name] node_str = node.render(parent, frozen) @@ -527,6 +531,11 @@ def get_parser(): 'Comma separated list of select packages to show ' 'in the output. 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.' + ), metavar='PACKAGES') parser.add_argument('-j', '--json', action='store_true', default=False, help=( 'Display dependency tree as json. This will yield ' @@ -548,10 +557,13 @@ def get_parser(): return parser -def main(): +def _get_args(): parser = get_parser() - args = parser.parse_args() + return parser.parse_args() + +def main(): + args = _get_args() pkgs = get_installed_distributions(local_only=args.local_only, user_only=args.user_only) @@ -600,10 +612,15 @@ def main(): return_code = 1 show_only = set(args.packages.split(',')) if args.packages else None + exclude = set(args.exclude.split(',')) if args.exclude else None + + if show_only and exclude and (show_only & exclude): + print('Conflicting packages found in --packages and --exclude lists.', file=sys.stderr) + sys.exit(1) tree = render_tree(tree if not args.reverse else reverse_tree(tree), list_all=args.all, show_only=show_only, - frozen=args.freeze) + frozen=args.freeze, exclude=exclude) print(tree) return return_code diff --git a/tests/test_pipdeptree.py b/tests/test_pipdeptree.py index 1e13920..94b4f49 100644 --- a/tests/test_pipdeptree.py +++ b/tests/test_pipdeptree.py @@ -6,11 +6,13 @@ from contextlib import contextmanager from tempfile import NamedTemporaryFile from operator import attrgetter +import pytest + from pipdeptree import (build_dist_index, construct_tree, DistPackage, ReqPackage, render_tree, reverse_tree, cyclic_deps, conflicting_deps, get_parser, render_json, render_json_tree, - dump_graphviz, print_graphviz) + dump_graphviz, print_graphviz, main) def venv_fixture(pickle_file): @@ -122,6 +124,42 @@ def test_render_tree_list_all(): assert 'itsdangerous==0.23' in lines +def test_render_tree_exclude(): + tree_str = render_tree(tree, list_all=True, exclude={'itsdangerous', 'SQLAlchemy', 'Flask', 'markupsafe', 'wheel'}) + assert tree_str == """\ +alembic==0.6.2 + - Mako [required: Any, installed: 0.9.1] +Flask-Script==0.6.6 +gnureadline==6.3.8 +ipython==2.0.0 +Jinja2==2.7.2 +Lookupy==0.1 +Mako==0.9.1 +psycopg2==2.7.3.2 +redis==2.9.1 +slugify==0.0.1 +Werkzeug==0.9.4""" + + +def test_render_tree_exclude_reverse(): + rtree = reverse_tree(tree) + + tree_str = render_tree(rtree, list_all=True, exclude={'itsdangerous', 'SQLAlchemy', 'Flask', 'markupsafe', 'wheel'}) + assert tree_str == """\ +alembic==0.6.2 +Flask-Script==0.6.6 +gnureadline==6.3.8 +ipython==2.0.0 +Jinja2==2.7.2 +Lookupy==0.1 +Mako==0.9.1 + - alembic==0.6.2 [requires: Mako] +psycopg2==2.7.3.2 +redis==2.9.1 +slugify==0.0.1 +Werkzeug==0.9.4""" + + def test_render_tree_freeze(): tree_str = render_tree(tree, list_all=False, frozen=True) lines = set() @@ -298,3 +336,37 @@ def test_conflicting_deps(): flask: [jinja], uritemplate: [simplejson], } + + +def test_main_basic(monkeypatch): + parser = get_parser() + args = parser.parse_args('') + + def _get_args(): + return args + monkeypatch.setattr('pipdeptree._get_args', _get_args) + + assert main() == 0 + + +def test_main_show_only_and_exclude_ok(monkeypatch): + parser = get_parser() + args = parser.parse_args('--packages Flask --exclude Jinja2'.split()) + + def _get_args(): + return args + monkeypatch.setattr('pipdeptree._get_args', _get_args) + + assert main() == 0 + + +def test_main_show_only_and_exclude_fails(monkeypatch): + parser = get_parser() + args = parser.parse_args('--packages Flask --exclude Jinja2,Flask'.split()) + + def _get_args(): + return args + monkeypatch.setattr('pipdeptree._get_args', _get_args) + + with pytest.raises(SystemExit): + main() diff --git a/tests/virtualenvs/testenv_requirements.txt b/tests/virtualenvs/testenv_requirements.txt index eeae223..4e9b266 100644 --- a/tests/virtualenvs/testenv_requirements.txt +++ b/tests/virtualenvs/testenv_requirements.txt @@ -7,7 +7,7 @@ MarkupSafe==0.18 SQLAlchemy==0.9.1 Werkzeug==0.9.4 alembic==0.6.2 -gnureadline==6.3.3 +gnureadline==6.3.8 ipython==2.0.0 itsdangerous==0.23 psycopg2==2.7.3.2