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

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`. 

11 

12"""Module that contains the command line application.""" 

13 

14from __future__ import annotations 

15 

16import argparse 

17import sys 

18from contextlib import contextmanager 

19from typing import List, Optional 

20 

21from colorama import init 

22 

23from dependenpy import __version__ 

24from dependenpy.dsm import DSM 

25from dependenpy.helpers import CSV, FORMAT, JSON, guess_depth 

26 

27 

28def get_parser() -> argparse.ArgumentParser: 

29 """ 

30 Return the CLI argument parser. 

31 

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) 

39 

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 ) 

134 

135 return parser 

136 

137 

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 

145 

146 

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 

156 

157 

158def _get_depth(opts, packages): 

159 return opts.depth or guess_depth(packages) 

160 

161 

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 

172 

173 

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) 

186 

187 

188def main(args: Optional[List[str]] = None) -> int: # noqa: WPS231 

189 """ 

190 Run the main program. 

191 

192 This function is executed when you type `dependenpy` or `python -m dependenpy`. 

193 

194 Arguments: 

195 args: Arguments passed from the command line. 

196 

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 

204 

205 dsm = DSM(*_get_packages(opts), build_tree=True, build_dependencies=True, enforce_init=not opts.greedy) 

206 if dsm.empty: 

207 return 1 

208 

209 # init colorama 

210 init() 

211 

212 try: 

213 _run(opts, dsm) 

214 except BrokenPipeError: 

215 # avoid traceback 

216 return 2 

217 

218 return 0