diff --git a/src/pipdeptree/__init__.py b/src/pipdeptree/__init__.py index 8b9fa09e..0e850163 100644 --- a/src/pipdeptree/__init__.py +++ b/src/pipdeptree/__init__.py @@ -455,7 +455,7 @@ def reverse(self): return PackageDAG(dict(m)) -def render_text(tree, list_all=True, frozen=False): +def render_text(tree, max_depth, list_all=True, frozen=False): """Print tree as text on console :param dict tree: the package tree @@ -471,12 +471,12 @@ def render_text(tree, list_all=True, frozen=False): nodes = [p for p in nodes if p.key not in branch_keys] if sys.stdout.encoding.lower() in ("utf-8", "utf-16", "utf-32"): - _render_text_with_unicode(tree, nodes, frozen) + _render_text_with_unicode(tree, nodes, max_depth, frozen) else: - _render_text_without_unicode(tree, nodes, frozen) + _render_text_without_unicode(tree, nodes, max_depth, frozen) -def _render_text_with_unicode(tree, nodes, frozen): +def _render_text_with_unicode(tree, nodes, max_depth, frozen): use_bullets = not frozen def aux( @@ -533,7 +533,7 @@ def aux( parent_is_last_child=is_last_child, ) for c in children - if c.project_name not in cur_chain + if c.project_name not in cur_chain and depth + 1 <= max_depth ] result += list(chain.from_iterable(children_strings)) @@ -543,10 +543,10 @@ def aux( print("\n".join(lines)) -def _render_text_without_unicode(tree, nodes, frozen): +def _render_text_without_unicode(tree, nodes, max_depth, frozen): use_bullets = not frozen - def aux(node, parent=None, indent=0, cur_chain=None): + def aux(node, parent=None, indent=0, cur_chain=None, depth=0): cur_chain = cur_chain or [] node_str = node.render(parent, frozen) if parent: @@ -554,9 +554,9 @@ def aux(node, parent=None, indent=0, cur_chain=None): node_str = prefix + node_str result = [node_str] children = [ - aux(c, node, indent=indent + 2, cur_chain=cur_chain + [c.project_name]) + aux(c, node, indent=indent + 2, cur_chain=cur_chain + [c.project_name], depth=depth + 1) for c in tree.get_children(node.key) - if c.project_name not in cur_chain + if c.project_name not in cur_chain and depth + 1 <= max_depth ] result += list(chain.from_iterable(children)) return result @@ -971,6 +971,16 @@ def get_parser(): "GraphViz, e.g.: dot, jpeg, pdf, png, svg" ), ) + parser.add_argument( + "-d", + "--depth", + type=lambda x: int(x) if x.isdigit() and (int(x) >= 0) else parser.error("Depth must be a number that is >= 0"), + default=float("inf"), + help=( + "Display dependency tree up to a depth >=0 using the default text display. All other display options" + " ignore this argument." + ), + ) return parser @@ -1077,6 +1087,6 @@ def main(): output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse) print_graphviz(output) else: - render_text(tree, args.all, args.freeze) + render_text(tree, args.depth, args.all, args.freeze) return return_code diff --git a/tests/test_pipdeptree.py b/tests/test_pipdeptree.py index 473eaa31..f50094d8 100644 --- a/tests/test_pipdeptree.py +++ b/tests/test_pipdeptree.py @@ -468,7 +468,104 @@ def test_render_text(capsys, list_all, reverse, unicode, expected_output): tree = t.reverse() if reverse else t encoding = "utf-8" if unicode else "ascii" with mock.patch("sys.stdout", MockStdout(encoding)): - p.render_text(tree, list_all=list_all, frozen=False) + p.render_text(tree, float("inf"), list_all=list_all, frozen=False) + captured = capsys.readouterr() + assert "\n".join(expected_output).strip() == captured.out.strip() + + +@pytest.mark.parametrize( + ("unicode", "level", "expected_output"), + [ + ( + True, + 0, + [ + "a==3.4.0", + "b==2.3.1", + "c==5.10.0", + "d==2.35", + "e==0.12.1", + "f==3.1", + "g==6.8.3rc1", + ], + ), + ( + False, + 0, + [ + "a==3.4.0", + "b==2.3.1", + "c==5.10.0", + "d==2.35", + "e==0.12.1", + "f==3.1", + "g==6.8.3rc1", + ], + ), + ( + True, + 2, + [ + "a==3.4.0", + "├── b [required: >=2.0.0, installed: 2.3.1]", + "│ └── d [required: >=2.30,<2.42, installed: 2.35]", + "└── c [required: >=5.7.1, installed: 5.10.0]", + " ├── d [required: >=2.30, installed: 2.35]", + " └── e [required: >=0.12.1, installed: 0.12.1]", + "b==2.3.1", + "└── d [required: >=2.30,<2.42, installed: 2.35]", + " └── e [required: >=0.9.0, installed: 0.12.1]", + "c==5.10.0", + "├── d [required: >=2.30, installed: 2.35]", + "│ └── e [required: >=0.9.0, installed: 0.12.1]", + "└── e [required: >=0.12.1, installed: 0.12.1]", + "d==2.35", + "└── e [required: >=0.9.0, installed: 0.12.1]", + "e==0.12.1", + "f==3.1", + "└── b [required: >=2.1.0, installed: 2.3.1]", + " └── d [required: >=2.30,<2.42, installed: 2.35]", + "g==6.8.3rc1", + "├── e [required: >=0.9.0, installed: 0.12.1]", + "└── f [required: >=3.0.0, installed: 3.1]", + " └── b [required: >=2.1.0, installed: 2.3.1]", + ], + ), + ( + False, + 2, + [ + "a==3.4.0", + " - b [required: >=2.0.0, installed: 2.3.1]", + " - d [required: >=2.30,<2.42, installed: 2.35]", + " - c [required: >=5.7.1, installed: 5.10.0]", + " - d [required: >=2.30, installed: 2.35]", + " - e [required: >=0.12.1, installed: 0.12.1]", + "b==2.3.1", + " - d [required: >=2.30,<2.42, installed: 2.35]", + " - e [required: >=0.9.0, installed: 0.12.1]", + "c==5.10.0", + " - d [required: >=2.30, installed: 2.35]", + " - e [required: >=0.9.0, installed: 0.12.1]", + " - e [required: >=0.12.1, installed: 0.12.1]", + "d==2.35", + " - e [required: >=0.9.0, installed: 0.12.1]", + "e==0.12.1", + "f==3.1", + " - b [required: >=2.1.0, installed: 2.3.1]", + " - d [required: >=2.30,<2.42, installed: 2.35]", + "g==6.8.3rc1", + " - e [required: >=0.9.0, installed: 0.12.1]", + " - f [required: >=3.0.0, installed: 3.1]", + " - b [required: >=2.1.0, installed: 2.3.1]", + ], + ), + ], +) +def test_render_text_given_depth(capsys, unicode, level, expected_output): + encoding = "utf-8" if unicode else "ascii" + with mock.patch("sys.stdout", MockStdout(encoding)): + p.render_text(t, level) captured = capsys.readouterr() assert "\n".join(expected_output).strip() == captured.out.strip() @@ -776,6 +873,27 @@ def test_parser_svg(): assert not args.json +@pytest.mark.parametrize( + ("should_be_error", "depth_arg", "expected_value"), + [ + (True, ["-d", "-1"], None), + (True, ["--depth", "string"], None), + (False, ["-d", "0"], 0), + (False, ["--depth", "8"], 8), + (False, [], float("inf")), + ], +) +def test_parser_depth(should_be_error, depth_arg, expected_value): + parser = p.get_parser() + + if should_be_error: + with pytest.raises(SystemExit): + parser.parse_args(depth_arg) + else: + args = parser.parse_args(depth_arg) + assert args.depth == expected_value + + @pytest.mark.parametrize("args_joined", [True, False]) def test_custom_interpreter(tmp_path, monkeypatch, capfd, args_joined): result = virtualenv.cli_run([str(tmp_path), "--activators", ""])