Repository: tox-dev/pipdeptree
Pull request #255 (2023-07-07 17:28:08)

Allow to force encoding
Resolves https://github.com/tox-dev/pipdeptree/issues/253


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 72eec5e..f0f132d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,7 +5,7 @@ repos:
       - id: end-of-file-fixer
       - id: trailing-whitespace
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: "v0.0.275"
+    rev: "v0.0.277"
     hooks:
       - id: ruff
         args: [--fix, --exit-non-zero-on-fix]
@@ -19,12 +19,12 @@ repos:
       - id: tox-ini-fmt
         args: ["-p", "fix"]
   - repo: https://github.com/tox-dev/pyproject-fmt
-    rev: "0.12.1"
+    rev: "0.13.0"
     hooks:
       - id: pyproject-fmt
-        additional_dependencies: ["tox>=4.6"]
+        additional_dependencies: ["tox>=4.6.4"]
   - repo: https://github.com/pre-commit/mirrors-prettier
-    rev: "v3.0.0-alpha.9-for-vscode"
+    rev: "v3.0.0"
     hooks:
       - id: prettier
         args: ["--print-width=120", "--prose-wrap=always"]
diff --git a/pyproject.toml b/pyproject.toml
index 863895c..30b53cb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,12 +45,12 @@ optional-dependencies.graphviz = [
 ]
 optional-dependencies.test = [
   "covdefaults>=2.3",
-  "diff-cover>=7.5",
+  "diff-cover>=7.6",
   "pip>=23.1.2",
-  "pytest>=7.3.1",
+  "pytest>=7.4",
   "pytest-cov>=4.1",
-  "pytest-mock>=3.10",
-  "virtualenv<21,>=20.23",
+  "pytest-mock>=3.11.1",
+  "virtualenv<21,>=20.23.1",
 ]
 urls.Changelog = "https://github.com/tox-dev/pipdeptree/blob/main/CHANGES.md"
 urls.Documentation = "https://github.com/tox-dev/pipdeptree/blob/main/README.md#pipdeptree"
diff --git a/src/pipdeptree/__init__.py b/src/pipdeptree/__init__.py
index 63f254d..56a353d 100644
--- a/src/pipdeptree/__init__.py
+++ b/src/pipdeptree/__init__.py
@@ -407,7 +407,7 @@ class PackageDAG(Mapping):
                 # as we're using array mutation
                 try:
                     node = [p for p in m if p.key == v.key][0]
-                except IndexError:
+                except IndexError:  # noqa: PERF203
                     node = v
                 m[node].append(k.as_parent_of(v))
             if k.key not in child_keys:
@@ -456,7 +456,7 @@ class ReversedPackageDAG(PackageDAG):
             for v in vs:
                 try:
                     node = [p for p in m if p.key == v.key][0]
-                except IndexError:
+                except IndexError:  # noqa: PERF203
                     node = v.as_parent_of(None)
                 m[node].append(k)
             if k.key not in child_keys:
@@ -464,7 +464,7 @@ class ReversedPackageDAG(PackageDAG):
         return PackageDAG(dict(m))
 
 
-def render_text(tree, max_depth, list_all=True, frozen=False):  # noqa: FBT002
+def render_text(tree, max_depth, encoding, list_all=True, frozen=False):  # noqa: FBT002
     """
     Print tree as text on console.
 
@@ -480,7 +480,7 @@ def render_text(tree, max_depth, list_all=True, frozen=False):  # noqa: FBT002
     if not list_all:
         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"):
+    if encoding in ("utf-8", "utf-16", "utf-32"):
         _render_text_with_unicode(tree, nodes, max_depth, frozen)
     else:
         _render_text_without_unicode(tree, nodes, max_depth, frozen)
@@ -828,7 +828,7 @@ def print_graphviz(dump_output):
             bytestream.write(dump_output)
 
 
-def conflicting_deps(tree):
+def conflicting_deps(tree: PackageDAG) -> dict[DistPackage, list[ReqPackage]]:
     """
     Returns dependencies which are not present or conflict with the requirements of other packages.
 
@@ -839,10 +839,10 @@ def conflicting_deps(tree):
     :rtype: dict
     """
     conflicting = defaultdict(list)
-    for p, rs in tree.items():
-        for req in rs:
+    for package, requires in tree.items():
+        for req in requires:
             if req.is_conflicting():
-                conflicting[p].append(req)
+                conflicting[package].append(req)  # noqa: PERF401
     return conflicting
 
 
@@ -997,6 +997,12 @@ def get_parser():
             " ignore this argument."
         ),
     )
+    parser.add_argument(
+        "--encoding",
+        dest="encoding_type",
+        default=sys.stdout.encoding,
+        help="Display dependency tree as text using specified encoding",
+    )
     return parser
 
 
@@ -1114,4 +1120,4 @@ def _render(args, tree):
         output = dump_graphviz(tree, output_format=args.output_format, is_reverse=args.reverse)
         print_graphviz(output)
     else:
-        render_text(tree, args.depth, args.all, args.freeze)
+        render_text(tree, args.depth, args.encoding_type, args.all, args.freeze)
diff --git a/tox.ini b/tox.ini
index 804b7b0..50fd7c5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -36,7 +36,7 @@ commands =
 description = format the code base to adhere to our styles, and complain about what we cannot do automatically
 skip_install = true
 deps =
-    pre-commit>=3.3.2
+    pre-commit>=3.3.3
 commands =
     pre-commit run --all-files --show-diff-on-failure
 


Test patch:
diff --git a/tests/test_pipdeptree.py b/tests/test_pipdeptree.py
index 2b04a67..e155b18 100644
--- a/tests/test_pipdeptree.py
+++ b/tests/test_pipdeptree.py
@@ -494,10 +494,9 @@ class MockStdout:
 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)):
-        render_text(tree, float("inf"), list_all=list_all, frozen=False)
-        captured = capsys.readouterr()
-        assert "\n".join(expected_output).strip() == captured.out.strip()
+    render_text(tree, float("inf"), encoding, list_all=list_all, frozen=False)
+    captured = capsys.readouterr()
+    assert "\n".join(expected_output).strip() == captured.out.strip()
 
 
 @pytest.mark.parametrize(
@@ -590,11 +589,162 @@ def test_render_text(capsys, list_all, reverse, unicode, expected_output):
     ],
 )
 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)):
-        render_text(t, level)
-        captured = capsys.readouterr()
-        assert "\n".join(expected_output).strip() == captured.out.strip()
+    render_text(t, level, encoding="utf-8" if unicode else "ascii")
+    captured = capsys.readouterr()
+    assert "\n".join(expected_output).strip() == captured.out.strip()
+
+
+@pytest.mark.parametrize(
+    ("level", "encoding", "expected_output"),
+    [
+        (
+            0,
+            "utf-8",
+            [
+                "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",
+            ],
+        ),
+        (
+            0,
+            "utf-8",
+            [
+                "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",
+            ],
+        ),
+        (
+            2,
+            "utf-8",
+            [
+                "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]",
+            ],
+        ),
+        (
+            2,
+            "ascii",
+            [
+                "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]",
+            ],
+        ),
+        (
+            2,
+            "utf-8",
+            [
+                "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]",
+            ],
+        ),
+        (
+            2,
+            "ascii",
+            [
+                "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_encoding(capsys, level, encoding, expected_output):
+    render_text(t, level, encoding, True, False)
+    captured = capsys.readouterr()
+    assert "\n".join(expected_output).strip() == captured.out.strip()
 
 
 # Tests for graph outputs