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
« 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
10import argparse
11import logging
12import os
13import platform
14import sys
15from typing import Any
17from snazzy import Snazzy, enable_colors
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
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)
41__version__ = get_project_info().get("version", "0.0.0")
43# --- verbose_parser ----------------------------------------------------------
45verbose_parser = argparse.ArgumentParser(
46 add_help=False,
47 # allow_abbrev=False,
48)
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)
62# --- common_parser ----------------------------------------------------------
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)
79# ===============================================================================
80# run
81# ===============================================================================
82def run() -> Any:
83 """CLI main entry point."""
85 parents = [verbose_parser, common_parser]
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")
101 # --- Create the parser for the "combine" command -----------------------------
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")
126 # --- Create the parser for the "info" command -----------------------------
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")
142 # --- Create the parser for the "purge" command ----------------------------
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")
152 # --- Create the parser for the "report" command ---------------------------
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 )
227 sp.set_defaults(cmd_handler=handle_report_command, cmd_name="report")
229 # --- Create the parser for the "run" command ------------------------------
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")
239 # --- Create the parser for the "tag" command ------------------------------
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")
279 # --- Parse command line ---------------------------------------------------
281 args = parser.parse_args()
283 args.verbose -= args.quiet
284 del args.quiet # type: ignore
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)
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
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)
314 if not callable(getattr(args, "cmd_handler", None)):
315 parser.error("missing command")
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...
329# Script entry point
330if __name__ == "__main__":
331 # Just in case...
332 from multiprocessing import freeze_support
334 freeze_support()
336 run()