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/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 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