Coverage for /Users/martin/prj/git/benchman_pre/src/benchman/cli.py: 0%

83 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-12-24 08:16 +0100

1# (c) 2024 Martin Wendt; see https://github.com/mar10/benchman 

2# Licensed under the MIT license: https://www.opensource.org/licenses/mit-license.php 

3""" 

4Usage examples: 

5 $ benchman --help 

6 $ benchman freeze 

7""" 

8# ruff: noqa: T201, T203 `print` found 

9 

10import argparse 

11import logging 

12import os 

13import platform 

14import sys 

15from typing import Any 

16 

17from snazzy import Snazzy, enable_colors 

18 

19from benchman.benchman import TAG_BASE, TAG_LATEST 

20from benchman.cli_commands import ( 

21 handle_combine_command, 

22 handle_info_command, 

23 handle_purge_command, 

24 handle_report_command, 

25 handle_run_command, 

26 handle_tag_command, 

27) 

28from benchman.util import PYTHON_VERSION, get_project_info, logger 

29 

30logging.basicConfig( 

31 level=logging.DEBUG, 

32 format="%(message)s", 

33 # format="%(asctime)-8s.%(msecs)-3d <%(thread)05d> %(levelname)-7s %(message)s", 

34 datefmt="%H:%M:%S", 

35) 

36# If basicConfig() was already called before, the above call was a no-op. 

37# Make sure, we adjust the level still: 

38# logging.root.setLevel(level) 

39 

40 

41__version__ = get_project_info().get("version", "0.0.0") 

42 

43# --- verbose_parser ---------------------------------------------------------- 

44 

45verbose_parser = argparse.ArgumentParser( 

46 add_help=False, 

47 # allow_abbrev=False, 

48) 

49 

50qv_group = verbose_parser.add_mutually_exclusive_group() 

51qv_group.add_argument( 

52 "-v", 

53 "--verbose", 

54 action="count", 

55 default=3, 

56 help="increment verbosity by one (default: %(default)s, range: 0..5)", 

57) 

58qv_group.add_argument( 

59 "-q", "--quiet", default=0, action="count", help="decrement verbosity by one" 

60) 

61 

62# --- common_parser ---------------------------------------------------------- 

63 

64common_parser = argparse.ArgumentParser( 

65 add_help=False, 

66 # allow_abbrev=False, 

67) 

68# common_parser.add_argument( 

69# "-n", 

70# "--dry-run", 

71# action="store_true", 

72# help="just simulate and log results, but don't change anything", 

73# ) 

74common_parser.add_argument( 

75 "--no-color", action="store_true", help="prevent use of ansi terminal color codes" 

76) 

77 

78 

79# =============================================================================== 

80# run 

81# =============================================================================== 

82def run() -> Any: 

83 """CLI main entry point.""" 

84 

85 parents = [verbose_parser, common_parser] 

86 

87 parser = argparse.ArgumentParser( 

88 description="Collect and analyze microbenchmarks.", 

89 epilog="See also https://github.com/mar10/benchman", 

90 parents=parents, 

91 allow_abbrev=False, 

92 ) 

93 parser.add_argument( 

94 "-V", 

95 "--version", 

96 action="store_true", 

97 help="display version info and exit (combine with -v for more information)", 

98 ) 

99 subparsers = parser.add_subparsers(help="sub-command help") 

100 

101 # --- Create the parser for the "combine" command ----------------------------- 

102 

103 sp = subparsers.add_parser( 

104 "combine", 

105 parents=parents, 

106 allow_abbrev=False, 

107 help="combine latest benchmark results into a single file", 

108 ) 

109 sp.add_argument( 

110 "--tag", 

111 default="latest", 

112 help="only combine benchmark files with this tag (default: %(default)s)", 

113 ) 

114 sp.add_argument( 

115 "--no-purge", 

116 action="store_true", 

117 help="keep single benchmark files after combining", 

118 ) 

119 sp.add_argument( 

120 "--force", 

121 action="store_true", 

122 help="overwrite existing combined file if any", 

123 ) 

124 sp.set_defaults(cmd_handler=handle_combine_command, cmd_name="combine") 

125 

126 # --- Create the parser for the "info" command ----------------------------- 

127 

128 sp = subparsers.add_parser( 

129 "info", 

130 parents=parents, 

131 allow_abbrev=False, 

132 help="dump information about the available benchmark results", 

133 ) 

134 sp.add_argument( 

135 "-l", 

136 "--list", 

137 action="store_true", 

138 help="list all benchmark files", 

139 ) 

140 sp.set_defaults(cmd_handler=handle_info_command, cmd_name="info") 

141 

142 # --- Create the parser for the "purge" command ---------------------------- 

143 

144 sp = subparsers.add_parser( 

145 "purge", 

146 parents=parents, 

147 allow_abbrev=False, 

148 help="remove latest uncombined benchmark results", 

149 ) 

150 sp.set_defaults(cmd_handler=handle_purge_command, cmd_name="purge") 

151 

152 # --- Create the parser for the "report" command --------------------------- 

153 

154 sp = subparsers.add_parser( 

155 "report", 

156 parents=parents, 

157 allow_abbrev=False, 

158 help="create a report from benchmark results", 

159 ) 

160 sp.add_argument( 

161 "--input", 

162 default="latest", 

163 help="input file name, path, or tag (default: %(default)s)", 

164 ) 

165 sp.add_argument( 

166 "--name", 

167 help="report title", 

168 ) 

169 sp.add_argument( 

170 "--columns", 

171 # default="full_name,version,python,best,ops,ops_rel,stdev", 

172 default="full_name,python,min,ops,ops_rel,stdev", 

173 help="comma separated list of columns to keep in the report " 

174 "(default: %(default)s)", 

175 ) 

176 sp.add_argument( 

177 "--dyn-col-name", 

178 help=( 

179 "benchmark dimension to create different columns for " 

180 "(e.g. 'sample_size','project', 'python', ...)" 

181 ), 

182 ) 

183 sp.add_argument( 

184 "--dyn-col-value", 

185 # default="ops", 

186 help=( 

187 "benchmark attribute to use as value for dynamic column rows " 

188 "(e.g. 'best', 'ops', 'mean', ...)" 

189 ), 

190 ) 

191 sp.add_argument( 

192 "--filter", 

193 help=( 

194 "filter rows by expression, e.g. " 

195 "'--filter \"python ^= 3.12, fullname *= bubble\"'" 

196 ), 

197 ) 

198 sp.add_argument( 

199 "--sort", 

200 help=( 

201 "comma separated list of columns to sort by. " 

202 "Prefix with '-' for descending (e.g. 'full_name,python,-ops')" 

203 ), 

204 ) 

205 sp.add_argument( 

206 "--format", 

207 # see https://tablib.readthedocs.io/en/stable/formats.html 

208 choices=["html", "markdown", "csv", "json", "yaml", "df"], 

209 default="markdown", 

210 help="report output format (default: %(default)s)", 

211 ) 

212 sp.add_argument( 

213 "--output", 

214 help="output file name (default: stdout)", 

215 ) 

216 sp.add_argument( 

217 "--append", 

218 action="store_true", 

219 help="append to existing file (overwrite otherwise)", 

220 ) 

221 sp.add_argument( 

222 "--open", 

223 action="store_true", 

224 help="open file in browser after successful run", 

225 ) 

226 

227 sp.set_defaults(cmd_handler=handle_report_command, cmd_name="report") 

228 

229 # --- Create the parser for the "run" command ------------------------------ 

230 

231 sp = subparsers.add_parser( 

232 "run", 

233 parents=parents, 

234 allow_abbrev=False, 

235 help="run and benchmark terminal applications", 

236 ) 

237 sp.set_defaults(cmd_handler=handle_run_command, cmd_name="run") 

238 

239 # --- Create the parser for the "tag" command ------------------------------ 

240 

241 sp = subparsers.add_parser( 

242 "tag", 

243 parents=parents, 

244 allow_abbrev=False, 

245 help="Copy existing benchmark results to make it persistent", 

246 ) 

247 sp.add_argument( 

248 "--source", 

249 default=TAG_LATEST, 

250 help=( 

251 "tag name of an existing file that will be copied " 

252 "(default: '%(default)s')" 

253 ), 

254 ) 

255 sp.add_argument( 

256 "-n", 

257 "--name", 

258 default=TAG_BASE, 

259 help="new tag name (default: '%(default)s')", 

260 ) 

261 sp.add_argument( 

262 "-f", 

263 "--force", 

264 action="store_true", 

265 help="overwrite existing tag file if any", 

266 ) 

267 sp.add_argument( 

268 "--keep-time", 

269 action="store_true", 

270 help="keep the original timestamp of the source file", 

271 ) 

272 sp.add_argument( 

273 "--git-add", 

274 action="store_true", 

275 help="run `git add -f` after successful tagging", 

276 ) 

277 sp.set_defaults(cmd_handler=handle_tag_command, cmd_name="tag") 

278 

279 # --- Parse command line --------------------------------------------------- 

280 

281 args = parser.parse_args() 

282 

283 args.verbose -= args.quiet 

284 del args.quiet # type: ignore 

285 

286 # print("verbose", args.verbose) 

287 # init_logging(args.verbose) # , args.log_file) 

288 if args.verbose >= 4: 

289 logger.setLevel(logging.DEBUG) 

290 else: 

291 logger.setLevel(logging.INFO) 

292 

293 if not args.no_color: 

294 # Enable terminal colors (if sys.stdout.isatty()) 

295 enable_colors(True, force=False) 

296 if os.environ.get("TERM_PROGRAM") == "vscode": 

297 Snazzy._support_emoji = True # VSCode can do 

298 

299 if getattr(args, "version", None): 

300 if args.verbose >= 4: 

301 version_info = "benchman/{} {}/{}({} bit) {}".format( 

302 __version__, 

303 platform.python_implementation(), 

304 PYTHON_VERSION, 

305 "64" if sys.maxsize > 2**32 else "32", 

306 platform.platform(aliased=False), 

307 ) 

308 version_info += f"\nPython from: {sys.executable}" 

309 else: 

310 version_info = __version__ 

311 print(version_info) 

312 sys.exit(0) 

313 

314 if not callable(getattr(args, "cmd_handler", None)): 

315 parser.error("missing command") 

316 

317 try: 

318 return args.cmd_handler(parser, args) 

319 # except click.ClickException as e: 

320 # print(f"{e!r}", file=sys.stderr) 

321 # sys.exit(2) 

322 # except (KeyboardInterrupt, click.Abort): 

323 except KeyboardInterrupt: 

324 print("\nAborted by user.", file=sys.stderr) 

325 sys.exit(3) 

326 # Unreachable... 

327 

328 

329# Script entry point 

330if __name__ == "__main__": 

331 # Just in case... 

332 from multiprocessing import freeze_support 

333 

334 freeze_support() 

335 

336 run()