Coverage for src/dependenpy/cli.py: 82.57%
79 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-09-04 11:35 +0200
« prev ^ index » next coverage.py v6.4.1, created at 2022-09-04 11:35 +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`.
12"""Module that contains the command line application."""
14from __future__ import annotations
16import argparse
17import sys
18from contextlib import contextmanager
19from typing import List, Optional
21from colorama import init
23from dependenpy import __version__
24from dependenpy.dsm import DSM
25from dependenpy.helpers import CSV, FORMAT, JSON, guess_depth
28def get_parser() -> argparse.ArgumentParser:
29 """
30 Return the CLI argument parser.
32 Returns:
33 An argparse parser.
34 """
35 parser = argparse.ArgumentParser(
36 prog="dependenpy", add_help=False, description="Command line tool for dependenpy Python package."
37 )
38 mxg = parser.add_mutually_exclusive_group(required=False)
40 parser.add_argument(
41 "packages",
42 metavar="PACKAGES",
43 nargs=argparse.ONE_OR_MORE,
44 help="The package list. Can be a comma-separated list. Each package "
45 "must be either a valid path or a package in PYTHONPATH.",
46 )
47 parser.add_argument(
48 "-d",
49 "--depth",
50 default=None,
51 type=int,
52 dest="depth",
53 help="Specify matrix or graph depth. Default: best guess.",
54 )
55 parser.add_argument(
56 "-f", "--format", choices=FORMAT, default="text", dest="format", help="Output format. Default: text."
57 )
58 mxg.add_argument(
59 "-g",
60 "--show-graph",
61 action="store_true",
62 dest="graph",
63 default=False,
64 help="Show the graph (no text format). Default: false.",
65 )
66 parser.add_argument(
67 "-G",
68 "--greedy",
69 action="store_true",
70 dest="greedy",
71 default=False,
72 help="Explore subdirectories even if they do not contain an "
73 "__init__.py file. Can make execution slower. Default: false.",
74 )
75 parser.add_argument(
76 "-h", "--help", action="help", default=argparse.SUPPRESS, help="Show this help message and exit."
77 )
78 parser.add_argument(
79 "-i",
80 "--indent",
81 default=None,
82 type=int,
83 dest="indent",
84 help="Specify output indentation. CSV will never be indented. "
85 "Text will always have new-lines. JSON can be minified with "
86 "a negative value. Default: best guess.",
87 )
88 mxg.add_argument(
89 "-l",
90 "--show-dependencies-list",
91 action="store_true",
92 dest="dependencies",
93 default=False,
94 help="Show the dependencies list. Default: false.",
95 )
96 mxg.add_argument(
97 "-m",
98 "--show-matrix",
99 action="store_true",
100 dest="matrix",
101 default=False,
102 help="Show the matrix. Default: true unless -g, -l or -t.",
103 )
104 parser.add_argument(
105 "-o",
106 "--output",
107 action="store",
108 dest="output",
109 default=sys.stdout,
110 help="Output to given file. Default: stdout.",
111 )
112 mxg.add_argument(
113 "-t",
114 "--show-treemap",
115 action="store_true",
116 dest="treemap",
117 default=False,
118 help="Show the treemap (work in progress). Default: false.",
119 )
120 parser.add_argument(
121 "-v",
122 "--version",
123 action="version",
124 version=f"dependenpy {__version__}",
125 help="Show the current version of the program and exit.",
126 )
127 parser.add_argument(
128 "-z",
129 "--zero",
130 dest="zero",
131 default="0",
132 help="Character to use for cells with value=0 (text matrix display only).",
133 )
135 return parser
138@contextmanager
139def _open_if_str(output):
140 if isinstance(output, str): 140 ↛ 141line 140 didn't jump to line 141, because the condition on line 140 was never true
141 with open(output, "w") as fd:
142 yield fd
143 else:
144 yield output
147def _get_indent(opts):
148 if opts.indent is None: 148 ↛ 152line 148 didn't jump to line 152, because the condition on line 148 was never false
149 if opts.format == CSV: 149 ↛ 150line 149 didn't jump to line 150, because the condition on line 149 was never true
150 return 0
151 return 2
152 elif opts.indent < 0 and opts.format == JSON:
153 # special case for json.dumps indent argument
154 return None
155 return opts.indent
158def _get_depth(opts, packages):
159 return opts.depth or guess_depth(packages)
162def _get_packages(opts): # noqa: WPS231
163 packages = []
164 for arg in opts.packages:
165 if "," in arg:
166 for package in arg.split(","):
167 if package not in packages:
168 packages.append(package)
169 elif arg not in packages: 169 ↛ 164line 169 didn't jump to line 164, because the condition on line 169 was never false
170 packages.append(arg)
171 return packages
174def _run(opts, dsm):
175 indent = _get_indent(opts)
176 depth = _get_depth(opts, packages=dsm.base_packages)
177 with _open_if_str(opts.output) as output:
178 if opts.dependencies:
179 dsm.print(format=opts.format, output=output, indent=indent)
180 elif opts.matrix:
181 dsm.print_matrix(format=opts.format, output=output, depth=depth, indent=indent, zero=opts.zero)
182 elif opts.treemap: 182 ↛ 184line 182 didn't jump to line 184, because the condition on line 182 was never false
183 dsm.print_treemap(format=opts.format, output=output)
184 elif opts.graph:
185 dsm.print_graph(format=opts.format, output=output, depth=depth, indent=indent)
188def main(args: Optional[List[str]] = None) -> int: # noqa: WPS231
189 """
190 Run the main program.
192 This function is executed when you type `dependenpy` or `python -m dependenpy`.
194 Arguments:
195 args: Arguments passed from the command line.
197 Returns:
198 An exit code. 0 (OK), 1 (dsm empty) or 2 (error).
199 """
200 parser = get_parser()
201 opts = parser.parse_args(args=args)
202 if not (opts.matrix or opts.dependencies or opts.treemap or opts.graph):
203 opts.matrix = True
205 dsm = DSM(*_get_packages(opts), build_tree=True, build_dependencies=True, enforce_init=not opts.greedy)
206 if dsm.empty:
207 return 1
209 # init colorama
210 init()
212 try:
213 _run(opts, dsm)
214 except BrokenPipeError:
215 # avoid traceback
216 return 2
218 return 0