Coverage for src/dependenpy/_internal/cli.py: 83.48%
85 statements
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-24 18:36 +0200
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-24 18:36 +0200
1# Why does this file exist, and why not put this in `__main__`?
2#
3# You might be tempted to import things from `__main__` later,
4# but that will cause problems: the code will get executed twice:
5#
6# - When you run `python -m dependenpy` python will execute
7# `__main__.py` as a script. That means there won't be any
8# `dependenpy.__main__` in `sys.modules`.
9# - When you import `__main__` it will get executed again (as a module) because
10# there's no `dependenpy.__main__` in `sys.modules`.
12from __future__ import annotations
14import argparse
15import sys
16from contextlib import contextmanager
17from typing import TYPE_CHECKING, Any, TextIO
19from colorama import init
21from dependenpy._internal import debug
22from dependenpy._internal.dsm import DSM
23from dependenpy._internal.helpers import CSV, FORMAT, JSON, guess_depth
25if TYPE_CHECKING:
26 from collections.abc import Iterator, Sequence
29class _DebugInfo(argparse.Action):
30 def __init__(self, nargs: int | str | None = 0, **kwargs: Any) -> None:
31 super().__init__(nargs=nargs, **kwargs)
33 def __call__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
34 debug._print_debug_info()
35 sys.exit(0)
38def get_parser() -> argparse.ArgumentParser:
39 """Return the CLI argument parser.
41 Returns:
42 An argparse parser.
43 """
44 parser = argparse.ArgumentParser(
45 prog="dependenpy",
46 add_help=False,
47 description="Command line tool for dependenpy Python package.",
48 )
49 mxg = parser.add_mutually_exclusive_group(required=False)
51 parser.add_argument(
52 "packages",
53 metavar="PACKAGES",
54 nargs=argparse.ONE_OR_MORE,
55 help="The package list. Can be a comma-separated list. Each package "
56 "must be either a valid path or a package in PYTHONPATH.",
57 )
58 parser.add_argument(
59 "-d",
60 "--depth",
61 default=None,
62 type=int,
63 dest="depth",
64 help="Specify matrix or graph depth. Default: best guess.",
65 )
66 parser.add_argument(
67 "-f",
68 "--format",
69 choices=FORMAT,
70 default="text",
71 dest="format",
72 help="Output format. Default: text.",
73 )
74 mxg.add_argument(
75 "-g",
76 "--show-graph",
77 action="store_true",
78 dest="graph",
79 default=False,
80 help="Show the graph (no text format). Default: false.",
81 )
82 parser.add_argument(
83 "-G",
84 "--greedy",
85 action="store_true",
86 dest="greedy",
87 default=False,
88 help="Explore subdirectories even if they do not contain an "
89 "__init__.py file. Can make execution slower. Default: false.",
90 )
91 parser.add_argument(
92 "-h",
93 "--help",
94 action="help",
95 default=argparse.SUPPRESS,
96 help="Show this help message and exit.",
97 )
98 parser.add_argument(
99 "-i",
100 "--indent",
101 default=None,
102 type=int,
103 dest="indent",
104 help="Specify output indentation. CSV will never be indented. "
105 "Text will always have new-lines. JSON can be minified with "
106 "a negative value. Default: best guess.",
107 )
108 mxg.add_argument(
109 "-l",
110 "--show-dependencies-list",
111 action="store_true",
112 dest="dependencies",
113 default=False,
114 help="Show the dependencies list. Default: false.",
115 )
116 mxg.add_argument(
117 "-m",
118 "--show-matrix",
119 action="store_true",
120 dest="matrix",
121 default=False,
122 help="Show the matrix. Default: true unless -g, -l or -t.",
123 )
124 parser.add_argument(
125 "-o",
126 "--output",
127 action="store",
128 dest="output",
129 default=sys.stdout,
130 help="Output to given file. Default: stdout.",
131 )
132 mxg.add_argument(
133 "-t",
134 "--show-treemap",
135 action="store_true",
136 dest="treemap",
137 default=False,
138 help="Show the treemap (work in progress). Default: false.",
139 )
140 parser.add_argument(
141 "-v",
142 "--version",
143 action="version",
144 version=f"dependenpy {debug._get_version()}",
145 help="Show the current version of the program and exit.",
146 )
147 parser.add_argument(
148 "-z",
149 "--zero",
150 dest="zero",
151 default="0",
152 help="Character to use for cells with value=0 (text matrix display only).",
153 )
155 parser.add_argument("--debug-info", action=_DebugInfo, help="Print debug information.")
156 return parser
159@contextmanager
160def _open_if_str(output: str | TextIO) -> Iterator[TextIO]:
161 if isinstance(output, str): 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true
162 with open(output, "w") as fd:
163 yield fd
164 else:
165 yield output
168def _get_indent(opts: argparse.Namespace) -> int | None:
169 if opts.indent is None: 169 ↛ 173line 169 didn't jump to line 173 because the condition on line 169 was always true
170 if opts.format == CSV: 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true
171 return 0
172 return 2
173 if opts.indent < 0 and opts.format == JSON:
174 # special case for json.dumps indent argument
175 return None
176 return opts.indent
179def _get_depth(opts: argparse.Namespace, packages: Sequence[str]) -> int:
180 return opts.depth or guess_depth(packages)
183def _get_packages(opts: argparse.Namespace) -> list[str]:
184 packages = []
185 for arg in opts.packages:
186 if "," in arg:
187 for package in arg.split(","):
188 if package not in packages:
189 packages.append(package)
190 elif arg not in packages: 190 ↛ 185line 190 didn't jump to line 185 because the condition on line 190 was always true
191 packages.append(arg)
192 return packages
195def _run(opts: argparse.Namespace, dsm: DSM) -> None:
196 indent = _get_indent(opts)
197 depth = _get_depth(opts, packages=dsm.base_packages)
198 with _open_if_str(opts.output) as output:
199 if opts.dependencies:
200 dsm.print(format=opts.format, output=output, indent=indent)
201 elif opts.matrix:
202 dsm.print_matrix(format=opts.format, output=output, depth=depth, indent=indent, zero=opts.zero)
203 elif opts.treemap: 203 ↛ 205line 203 didn't jump to line 205 because the condition on line 203 was always true
204 dsm.print_treemap(format=opts.format, output=output)
205 elif opts.graph:
206 dsm.print_graph(format=opts.format, output=output, depth=depth, indent=indent)
209def main(args: list[str] | None = None) -> int:
210 """Run the main program.
212 This function is executed when you type `dependenpy` or `python -m dependenpy`.
214 Parameters:
215 args: Arguments passed from the command line.
217 Returns:
218 An exit code. 0 (OK), 1 (dsm empty) or 2 (error).
219 """
220 parser = get_parser()
221 opts = parser.parse_args(args=args)
222 if not (opts.matrix or opts.dependencies or opts.treemap or opts.graph):
223 opts.matrix = True
225 dsm = DSM(*_get_packages(opts), build_tree=True, build_dependencies=True, enforce_init=not opts.greedy)
226 if dsm.empty:
227 return 1
229 # init colorama
230 init()
232 try:
233 _run(opts, dsm)
234 except BrokenPipeError:
235 # avoid traceback
236 return 2
238 return 0