Repository: tox-dev/pipdeptree
Pull request #271 (2023-07-17 01:42:01)

Fix graphviz CLI failing
Resolves https://github.com/tox-dev/pipdeptree/issues/269


Patch:
diff --git a/pyproject.toml b/pyproject.toml
index d2e44f6..7c568d7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -95,6 +95,7 @@ html.skip_covered = false
 paths.source = ["src", ".tox/*/lib/python*/site-packages", "*/src"]
 run.parallel = true
 run.plugins = ["covdefaults"]
+report.fail_under = 89
 subtract_omit = "*/__main__.py"
 
 [tool.mypy]
diff --git a/src/pipdeptree/__main__.py b/src/pipdeptree/__main__.py
index 07f2b1e..5059487 100644
--- a/src/pipdeptree/__main__.py
+++ b/src/pipdeptree/__main__.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import sys
+from typing import Sequence
 
 from pipdeptree._cli import get_options
 from pipdeptree._discovery import get_installed_distributions
@@ -11,29 +12,29 @@ from pipdeptree._render import render
 from ._validate import validate
 
 
-def main() -> None | int:
-    args = get_options()
-    result = handle_non_host_target(args)
+def main(args: Sequence[str] | None = None) -> None | int:
+    options = get_options(args)
+    result = handle_non_host_target(options)
     if result is not None:
         return result
 
-    pkgs = get_installed_distributions(local_only=args.local_only, user_only=args.user_only)
+    pkgs = get_installed_distributions(local_only=options.local_only, user_only=options.user_only)
     tree = PackageDAG.from_pkgs(pkgs)
-    is_text_output = not any([args.json, args.json_tree, args.output_format])
+    is_text_output = not any([options.json, options.json_tree, options.output_format])
 
-    return_code = validate(args, is_text_output, tree)
+    return_code = validate(options, is_text_output, tree)
 
     # Reverse the tree (if applicable) before filtering, thus ensuring, that the filter will be applied on ReverseTree
-    if args.reverse:
+    if options.reverse:
         tree = tree.reverse()
 
-    show_only = set(args.packages.split(",")) if args.packages else None
-    exclude = set(args.exclude.split(",")) if args.exclude else None
+    show_only = set(options.packages.split(",")) if options.packages else None
+    exclude = set(options.exclude.split(",")) if options.exclude else None
 
     if show_only is not None or exclude is not None:
         tree = tree.filter_nodes(show_only, exclude)
 
-    render(args, tree)
+    render(options, tree)
 
     return return_code
 
diff --git a/src/pipdeptree/_cli.py b/src/pipdeptree/_cli.py
index a944221..a997aeb 100644
--- a/src/pipdeptree/_cli.py
+++ b/src/pipdeptree/_cli.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import sys
 from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
-from typing import TYPE_CHECKING, cast
+from typing import TYPE_CHECKING, Sequence, cast
 
 from .version import __version__
 
@@ -22,7 +22,7 @@ class Options(Namespace):
     json: bool
     json_tree: bool
     mermaid: bool
-    graph_output: str | None
+    output_format: str | None
     depth: float
     encoding: str
 
@@ -135,9 +135,9 @@ def build_parser() -> ArgumentParser:
     return parser
 
 
-def get_options() -> Options:
+def get_options(args: Sequence[str] | None) -> Options:
     parser = build_parser()
-    return cast(Options, parser.parse_args())
+    return cast(Options, parser.parse_args(args))
 
 
 __all__ = [
diff --git a/src/pipdeptree/_render/__init__.py b/src/pipdeptree/_render/__init__.py
index da3e058..7693617 100644
--- a/src/pipdeptree/_render/__init__.py
+++ b/src/pipdeptree/_render/__init__.py
@@ -21,10 +21,16 @@ def render(options: Options, tree: PackageDAG) -> None:
     elif options.mermaid:
         print(render_mermaid(tree))  # noqa: T201
     elif options.output_format:
-        assert options.graph_output is not None  # noqa: S101
-        render_graphviz(tree, output_format=options.graph_output, reverse=options.reverse)
+        assert options.output_format is not None  # noqa: S101
+        render_graphviz(tree, output_format=options.output_format, reverse=options.reverse)
     else:
-        render_text(tree, options.depth, options.encoding_type, options.all, options.freeze)
+        render_text(
+            tree,
+            max_depth=options.depth,
+            encoding=options.encoding_type,
+            list_all=options.all,
+            frozen=options.freeze,
+        )
 
 
 __all__ = [
diff --git a/src/pipdeptree/_render/text.py b/src/pipdeptree/_render/text.py
index 9cc4551..d1a1c34 100644
--- a/src/pipdeptree/_render/text.py
+++ b/src/pipdeptree/_render/text.py
@@ -9,10 +9,11 @@ if TYPE_CHECKING:
 
 def render_text(
     tree: PackageDAG,
+    *,
     max_depth: float,
     encoding: str,
-    list_all: bool = True,  # noqa: FBT001, FBT002
-    frozen: bool = False,  # noqa: FBT001, FBT002
+    list_all: bool = True,
+    frozen: bool = False,
 ) -> None:
     """
     Print tree as text on console.
diff --git a/tox.ini b/tox.ini
index 30328ae..7ce6bc5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -25,12 +25,12 @@ set_env =
     _COVERAGE_SRC = {envsitepackagesdir}/sphinx_argparse_cli
 commands =
     python -m pytest {tty:--color=yes} {posargs: \
-      --junitxml {toxworkdir}{/}junit.{envname}.xml --cov {envsitepackagesdir}{/}pipdeptree \
-      --cov {toxinidir}{/}tests --cov-fail-under=85 \
+      --cov {envsitepackagesdir}{/}pipdeptree --cov {toxinidir}{/}tests \
       --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \
       --cov-report html:{envtmpdir}{/}htmlcov --cov-report xml:{toxworkdir}{/}coverage.{envname}.xml \
+      --junitxml {toxworkdir}{/}junit.{envname}.xml \
       tests}
-    diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}{/}coverage.{envname}.xml
+    diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}{/}coverage.{envname}.xml --fail-under 100
 
 [testenv:fix]
 description = format the code base to adhere to our styles, and complain about what we cannot do automatically


Test patch:
diff --git a/tests/render/test_render.py b/tests/render/test_render.py
new file mode 100644
index 0000000..acef413
--- /dev/null
+++ b/tests/render/test_render.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from math import inf
+from typing import TYPE_CHECKING
+from unittest.mock import ANY
+
+from pipdeptree.__main__ import main
+
+if TYPE_CHECKING:
+    from pytest_mock import MockerFixture
+
+
+def test_json_routing(mocker: MockerFixture) -> None:
+    render = mocker.patch("pipdeptree._render.render_json")
+    main(["--json"])
+    render.assert_called_once_with(ANY)
+
+
+def test_json_tree_routing(mocker: MockerFixture) -> None:
+    render = mocker.patch("pipdeptree._render.render_json_tree")
+    main(["--json-tree"])
+    render.assert_called_once_with(ANY)
+
+
+def test_mermaid_routing(mocker: MockerFixture) -> None:
+    render = mocker.patch("pipdeptree._render.render_mermaid")
+    main(["--mermaid"])
+    render.assert_called_once_with(ANY)
+
+
+def test_grahpviz_routing(mocker: MockerFixture) -> None:
+    render = mocker.patch("pipdeptree._render.render_graphviz")
+    main(["--graph-output", "dot"])
+    render.assert_called_once_with(ANY, output_format="dot", reverse=False)
+
+
+def test_text_routing(mocker: MockerFixture) -> None:
+    render = mocker.patch("pipdeptree._render.render_text")
+    main([])
+    render.assert_called_once_with(ANY, encoding="utf-8", frozen=False, list_all=False, max_depth=inf)
diff --git a/tests/render/test_text.py b/tests/render/test_text.py
index 220d9db..5accf0b 100644
--- a/tests/render/test_text.py
+++ b/tests/render/test_text.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import sys
 from typing import TYPE_CHECKING
 
 import pytest
@@ -11,20 +10,6 @@ if TYPE_CHECKING:
     from pipdeptree._models import PackageDAG
 
 
-class MockStdout:
-    """
-    A wrapper to stdout that mocks the `encoding` attribute (to have `render_text()` render with unicode/non-unicode)
-    and `write()` (so that `print()` calls can write to stdout).
-    """
-
-    def __init__(self, encoding: str) -> None:
-        self.stdout = sys.stdout
-        self.encoding = encoding
-
-    def write(self, text: str) -> None:
-        self.stdout.write(text)
-
-
 @pytest.mark.parametrize(
     ("list_all", "reverse", "unicode", "expected_output"),
     [
@@ -258,7 +243,7 @@ def test_render_text(  # noqa: PLR0913
 ) -> None:
     tree = example_dag.reverse() if reverse else example_dag
     encoding = "utf-8" if unicode else "ascii"
-    render_text(tree, float("inf"), encoding, list_all=list_all, frozen=False)
+    render_text(tree, max_depth=float("inf"), encoding=encoding, list_all=list_all, frozen=False)
     captured = capsys.readouterr()
     assert "\n".join(expected_output).strip() == captured.out.strip()
 
@@ -359,7 +344,7 @@ def test_render_text_given_depth(
     expected_output: list[str],
     example_dag: PackageDAG,
 ) -> None:
-    render_text(example_dag, level, encoding="utf-8" if unicode else "ascii")
+    render_text(example_dag, max_depth=level, encoding="utf-8" if unicode else "ascii")
     captured = capsys.readouterr()
     assert "\n".join(expected_output).strip() == captured.out.strip()
 
@@ -518,6 +503,6 @@ def test_render_text_encoding(
     expected_output: list[str],
     example_dag: PackageDAG,
 ) -> None:
-    render_text(example_dag, level, encoding, True, False)
+    render_text(example_dag, max_depth=level, encoding=encoding, list_all=True, frozen=False)
     captured = capsys.readouterr()
     assert "\n".join(expected_output).strip() == captured.out.strip()