Skip to content

Commit 745b680

Browse files
authored
Implement --output to centralize choosing an output format (#519)
Resolves #349.
1 parent f4bf9c7 commit 745b680

File tree

8 files changed

+173
-109
lines changed

8 files changed

+173
-109
lines changed

README.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -221,42 +221,47 @@ In earlier versions, `--json`, `--json-tree` and `--graph-output` options overri
221221

222222
## Usage
223223

224-
```bash
224+
```text
225225
% pipdeptree --help
226-
usage: pipdeptree [-h] [-v] [-w [{silence,suppress,fail}]] [--python PYTHON] [--path PATH] [-p P] [-e P] [-l | -u] [-f] [--encoding E] [-a] [-d D] [-r] [--license] [-j | --json-tree | --mermaid | --graph-output FMT]
226+
usage: pipdeptree [-h] [-v] [-w {silence,suppress,fail}] [--python PYTHON] [--path PATH] [-p P] [-e P] [--exclude-dependencies] [-l | -u] [-f] [--encoding E] [-a] [-d D] [-r] [--license]
227+
[-j | --json-tree | --mermaid | --graph-output FMT | -o FMT]
227228
228229
Dependency tree of the installed python packages
229230
230231
options:
231232
-h, --help show this help message and exit
232233
-v, --version show program's version number and exit
233-
-w [{silence,suppress,fail}], --warn [{silence,suppress,fail}]
234+
-w {silence,suppress,fail}, --warn {silence,suppress,fail}
234235
warning control: suppress will show warnings but return 0 whether or not they are present; silence will not show warnings at all and always return 0; fail will show warnings and return 1 if any are present (default:
235236
suppress)
236237
237238
select:
238239
choose what to render
239240
240241
--python PYTHON Python interpreter to inspect. With "auto", it attempts to detect your virtual environment and fails if it can't. (default: /usr/local/bin/python)
241-
--path PATH Passes a path used to restrict where packages should be looked for (can be used multiple times) (default: None)
242+
--path PATH passes a path used to restrict where packages should be looked for (can be used multiple times) (default: None)
242243
-p P, --packages P comma separated list of packages to show - wildcards are supported, like 'somepackage.*' (default: None)
243244
-e P, --exclude P comma separated list of packages to not show - wildcards are supported, like 'somepackage.*'. (cannot combine with -p or -a) (default: None)
245+
--exclude-dependencies
246+
used along with --exclude to also exclude dependencies of packages (default: False)
244247
-l, --local-only if in a virtualenv that has global access do not show globally installed packages (default: False)
245248
-u, --user-only only show installations in the user site dir (default: False)
246249
247250
render:
248-
choose how to render the dependency tree (by default will use text mode)
251+
choose how to render the dependency tree
249252
250-
-f, --freeze print names so as to write freeze files (default: False)
253+
-f, --freeze (Deprecated, use -o) print names so as to write freeze files (default: False)
251254
--encoding E the encoding to use when writing to the output (default: utf-8)
252255
-a, --all list all deps at top level (text and freeze render only) (default: False)
253256
-d D, --depth D limit the depth of the tree (text and freeze render only) (default: inf)
254257
-r, --reverse render the dependency tree in the reverse fashion ie. the sub-dependencies are listed with the list of packages that need them under them (default: False)
255258
--license list the license(s) of a package (text render only) (default: False)
256-
-j, --json raw JSON - this will yield output that may be used by external tools (default: False)
257-
--json-tree nested JSON - mimics the text format layout (default: False)
258-
--mermaid https://mermaid.js.org flow diagram (default: False)
259-
--graph-output FMT Graphviz rendering with the value being the graphviz output e.g.: dot, jpeg, pdf, png, svg (default: None)
259+
-j, --json (Deprecated, use -o) raw JSON - this will yield output that may be used by external tools (default: False)
260+
--json-tree (Deprecated, use -o) nested JSON - mimics the text format layout (default: False)
261+
--mermaid (Deprecated, use -o) https://mermaid.js.org flow diagram (default: False)
262+
--graph-output FMT (Deprecated, use -o) Graphviz rendering with the value being the graphviz output e.g.: dot, jpeg, pdf, png, svg (default: None)
263+
-o FMT, --output FMT
264+
specify how to render the tree; supported formats: freeze, json, json-tree, mermaid, text, or graphviz-* (e.g. graphviz-png, graphviz-dot) (default: text)
260265
```
261266

262267
## Known issues

src/pipdeptree/__main__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66
from typing import TYPE_CHECKING
77

8-
from pipdeptree._cli import get_options
8+
from pipdeptree._cli import Options, get_options
99
from pipdeptree._detect_env import detect_active_interpreter
1010
from pipdeptree._discovery import InterpreterQueryError, get_installed_distributions
1111
from pipdeptree._models import PackageDAG
@@ -23,8 +23,7 @@ def main(args: Sequence[str] | None = None) -> int | None:
2323
options = get_options(args)
2424

2525
# Warnings are only enabled when using text output.
26-
is_text_output = not any([options.json, options.json_tree, options.output_format])
27-
if not is_text_output:
26+
if not _is_text_output(options):
2827
options.warn = "silence"
2928
warning_printer = get_warning_printer()
3029
warning_printer.warning_type = WarningType.from_str(options.warn)
@@ -72,6 +71,12 @@ def main(args: Sequence[str] | None = None) -> int | None:
7271
return _determine_return_code(warning_printer)
7372

7473

74+
def _is_text_output(options: Options) -> bool:
75+
if any([options.json, options.json_tree, options.graphviz_format, options.mermaid]):
76+
return False
77+
return options.output_format in {"freeze", "text"}
78+
79+
7580
def _determine_return_code(warning_printer: WarningPrinter) -> int:
7681
return 1 if warning_printer.has_warned_with_failure() else 0
7782

src/pipdeptree/_cli.py

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import sys
4-
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
4+
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, ArgumentTypeError, Namespace
55
from typing import TYPE_CHECKING, cast
66

77
from .version import __version__
@@ -25,12 +25,17 @@ class Options(Namespace):
2525
json: bool
2626
json_tree: bool
2727
mermaid: bool
28-
output_format: str | None
28+
graphviz_format: str | None
29+
output_format: str
2930
depth: float
3031
encoding: str
3132
license: bool
3233

3334

35+
# NOTE: graphviz-* has been intentionally left out. Users of this var should handle it separately.
36+
ALLOWED_RENDER_FORMATS = ["freeze", "json", "json-tree", "mermaid", "text"]
37+
38+
3439
class _Formatter(ArgumentDefaultsHelpFormatter):
3540
def __init__(self, prog: str) -> None:
3641
super().__init__(prog, max_help_position=22, width=240)
@@ -63,7 +68,7 @@ def build_parser() -> ArgumentParser:
6368
)
6469
select.add_argument(
6570
"--path",
66-
help="Passes a path used to restrict where packages should be looked for (can be used multiple times)",
71+
help="passes a path used to restrict where packages should be looked for (can be used multiple times)",
6772
action="append",
6873
)
6974
select.add_argument(
@@ -81,7 +86,7 @@ def build_parser() -> ArgumentParser:
8186
)
8287
select.add_argument(
8388
"--exclude-dependencies",
84-
help="Used along with --exclude to also exclude dependencies of packages",
89+
help="used along with --exclude to also exclude dependencies of packages",
8590
action="store_true",
8691
)
8792

@@ -96,9 +101,11 @@ def build_parser() -> ArgumentParser:
96101

97102
render = parser.add_argument_group(
98103
title="render",
99-
description="choose how to render the dependency tree (by default will use text mode)",
104+
description="choose how to render the dependency tree",
105+
)
106+
render.add_argument(
107+
"-f", "--freeze", action="store_true", help="(Deprecated, use -o) print names so as to write freeze files"
100108
)
101-
render.add_argument("-f", "--freeze", action="store_true", help="print names so as to write freeze files")
102109
render.add_argument(
103110
"--encoding",
104111
dest="encoding",
@@ -139,41 +146,79 @@ def build_parser() -> ArgumentParser:
139146
"--json",
140147
action="store_true",
141148
default=False,
142-
help="raw JSON - this will yield output that may be used by external tools",
149+
help="(Deprecated, use -o) raw JSON - this will yield output that may be used by external tools",
143150
)
144151
render_type.add_argument(
145152
"--json-tree",
146153
action="store_true",
147154
default=False,
148-
help="nested JSON - mimics the text format layout",
155+
help="(Deprecated, use -o) nested JSON - mimics the text format layout",
149156
)
150157
render_type.add_argument(
151158
"--mermaid",
152159
action="store_true",
153160
default=False,
154-
help="https://mermaid.js.org flow diagram",
161+
help="(Deprecated, use -o) https://mermaid.js.org flow diagram",
155162
)
156163
render_type.add_argument(
157164
"--graph-output",
158165
metavar="FMT",
166+
dest="graphviz_format",
167+
help="(Deprecated, use -o) Graphviz rendering with the value being the graphviz output e.g.:\
168+
dot, jpeg, pdf, png, svg",
169+
)
170+
render_type.add_argument(
171+
"-o",
172+
"--output",
173+
metavar="FMT",
159174
dest="output_format",
160-
help="Graphviz rendering with the value being the graphviz output e.g.: dot, jpeg, pdf, png, svg",
175+
type=_validate_output_format,
176+
default="text",
177+
help=f"specify how to render the tree; supported formats: {', '.join(ALLOWED_RENDER_FORMATS)}, or graphviz-*\
178+
(e.g. graphviz-png, graphviz-dot)",
161179
)
162180
return parser
163181

164182

165183
def get_options(args: Sequence[str] | None) -> Options:
166184
parser = build_parser()
167185
parsed_args = parser.parse_args(args)
186+
options = cast("Options", parsed_args)
168187

169-
if parsed_args.exclude_dependencies and not parsed_args.exclude:
188+
options.output_format = _handle_legacy_render_options(options)
189+
190+
if options.exclude_dependencies and not options.exclude:
170191
return parser.error("must use --exclude-dependencies with --exclude")
171-
if parsed_args.license and parsed_args.freeze:
192+
if options.license and options.freeze:
172193
return parser.error("cannot use --license with --freeze")
173-
if parsed_args.path and (parsed_args.local_only or parsed_args.user_only):
194+
if options.path and (options.local_only or options.user_only):
174195
return parser.error("cannot use --path with --user-only or --local-only")
175196

176-
return cast("Options", parsed_args)
197+
return options
198+
199+
200+
def _handle_legacy_render_options(options: Options) -> str:
201+
if options.freeze:
202+
return "freeze"
203+
if options.json:
204+
return "json"
205+
if options.json_tree:
206+
return "json-tree"
207+
if options.mermaid:
208+
return "mermaid"
209+
if options.graphviz_format:
210+
return f"graphviz-{options.graphviz_format}"
211+
212+
return options.output_format
213+
214+
215+
def _validate_output_format(value: str) -> str:
216+
if value in ALLOWED_RENDER_FORMATS:
217+
return value
218+
if value.startswith("graphviz-"):
219+
return value
220+
msg = f'"{value}" is not a known output format. Must be one of {", ".join(ALLOWED_RENDER_FORMATS)}, or graphviz-*'
221+
raise ArgumentTypeError(msg)
177222

178223

179224
__all__ = [

src/pipdeptree/_models/package.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def licenses(self) -> str:
3939
except PackageNotFoundError:
4040
return self.UNKNOWN_LICENSE_STR
4141

42-
if license_str := dist_metadata.get("License-Expression"):
42+
if license_str := dist_metadata[("License-Expression")]:
4343
return f"({license_str})"
4444

4545
license_strs: list[str] = []

src/pipdeptree/_render/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@
1515

1616

1717
def render(options: Options, tree: PackageDAG) -> None:
18-
if options.json:
18+
output_format = options.output_format
19+
if output_format == "json":
1920
render_json(tree)
20-
elif options.json_tree:
21+
elif output_format == "json-tree":
2122
render_json_tree(tree)
22-
elif options.mermaid:
23+
elif output_format == "mermaid":
2324
render_mermaid(tree)
24-
elif options.output_format:
25-
render_graphviz(tree, output_format=options.output_format, reverse=options.reverse)
26-
elif options.freeze:
25+
elif output_format == "freeze":
2726
render_freeze(tree, max_depth=options.depth, list_all=options.all)
27+
elif output_format.startswith("graphviz-"):
28+
render_graphviz(tree, output_format=output_format[len("graphviz-") :], reverse=options.reverse)
2829
else:
2930
render_text(
3031
tree,

tests/_models/test_package.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,13 @@ def test_dist_package_as_dict() -> None:
107107
("mocked_metadata", "expected_output"),
108108
[
109109
pytest.param(
110-
Mock(get=Mock(return_value=None), get_all=Mock(return_value=[])),
110+
Mock(__getitem__=Mock(return_value=None), get_all=Mock(return_value=[])),
111111
Package.UNKNOWN_LICENSE_STR,
112112
id="no-license",
113113
),
114114
pytest.param(
115115
Mock(
116-
get=Mock(return_value=None),
116+
__getitem__=Mock(return_value=None),
117117
get_all=Mock(
118118
return_value=[
119119
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
@@ -126,7 +126,7 @@ def test_dist_package_as_dict() -> None:
126126
),
127127
pytest.param(
128128
Mock(
129-
get=Mock(return_value=None),
129+
__getitem__=Mock(return_value=None),
130130
get_all=Mock(
131131
return_value=[
132132
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
@@ -138,13 +138,13 @@ def test_dist_package_as_dict() -> None:
138138
id="more-than-one-license",
139139
),
140140
pytest.param(
141-
Mock(get=Mock(return_value="MIT"), get_all=Mock(return_value=[])),
141+
Mock(__getitem__=Mock(return_value="MIT"), get_all=Mock(return_value=[])),
142142
"(MIT)",
143143
id="license-expression",
144144
),
145145
pytest.param(
146146
Mock(
147-
get=Mock(return_value="MIT"),
147+
__getitem__=Mock(return_value="MIT"),
148148
get_all=Mock(
149149
return_value=[
150150
"License :: OSI Approved :: MIT License",

tests/render/test_render.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,51 @@
44
from typing import TYPE_CHECKING
55
from unittest.mock import ANY
66

7+
import pytest
8+
79
from pipdeptree.__main__ import main
810

911
if TYPE_CHECKING:
1012
from pytest_mock import MockerFixture
1113

1214

13-
def test_json_routing(mocker: MockerFixture) -> None:
15+
@pytest.mark.parametrize("option", [["--json"], ["--output", "json"]])
16+
def test_json_routing(option: list[str], mocker: MockerFixture) -> None:
1417
render = mocker.patch("pipdeptree._render.render_json")
15-
main(["--json"])
18+
main(option)
1619
render.assert_called_once_with(ANY)
1720

1821

19-
def test_json_tree_routing(mocker: MockerFixture) -> None:
22+
@pytest.mark.parametrize("option", [["--json-tree"], ["--output", "json-tree"]])
23+
def test_json_tree_routing(option: list[str], mocker: MockerFixture) -> None:
2024
render = mocker.patch("pipdeptree._render.render_json_tree")
21-
main(["--json-tree"])
25+
main(option)
2226
render.assert_called_once_with(ANY)
2327

2428

25-
def test_mermaid_routing(mocker: MockerFixture) -> None:
29+
@pytest.mark.parametrize("option", [["--mermaid"], ["--output", "mermaid"]])
30+
def test_mermaid_routing(option: list[str], mocker: MockerFixture) -> None:
2631
render = mocker.patch("pipdeptree._render.render_mermaid")
27-
main(["--mermaid"])
32+
main(option)
2833
render.assert_called_once_with(ANY)
2934

3035

31-
def test_grahpviz_routing(mocker: MockerFixture) -> None:
36+
@pytest.mark.parametrize("option", [["--graph-output", "dot"], ["--output", "graphviz-dot"]])
37+
def test_grahpviz_routing(option: list[str], mocker: MockerFixture) -> None:
3238
render = mocker.patch("pipdeptree._render.render_graphviz")
33-
main(["--graph-output", "dot"])
39+
main(option)
3440
render.assert_called_once_with(ANY, output_format="dot", reverse=False)
3541

3642

37-
def test_text_routing(mocker: MockerFixture) -> None:
43+
@pytest.mark.parametrize("option", [[], ["--output", "text"]])
44+
def test_text_routing(option: list[str], mocker: MockerFixture) -> None:
3845
render = mocker.patch("pipdeptree._render.render_text")
39-
main([])
46+
main(option)
4047
render.assert_called_once_with(ANY, encoding="utf-8", max_depth=inf, list_all=False, include_license=False)
4148

4249

43-
def test_freeze_routing(mocker: MockerFixture) -> None:
50+
@pytest.mark.parametrize("option", [["--freeze"], ["--output", "freeze"]])
51+
def test_freeze_routing(option: list[str], mocker: MockerFixture) -> None:
4452
render = mocker.patch("pipdeptree._render.render_freeze")
45-
main(["--freeze"])
53+
main(option)
4654
render.assert_called_once_with(ANY, max_depth=inf, list_all=False)

0 commit comments

Comments
 (0)