Coverage for src/hatch_ci/cli.py: 0%
72 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-01 20:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-01 20:04 +0000
1from __future__ import annotations
3import argparse
4import functools
5import logging
6import sys
7from typing import Any, Callable, Protocol
9from . import tools
12class ErrorFn(Protocol):
13 def __call__(self, message: str, explain: str | None, hint: str | None) -> None:
14 ...
17class AbortExecutionError(Exception):
18 @staticmethod
19 def _strip(txt):
20 txt = txt or ""
21 txt = txt[1:] if txt.startswith("\n") else txt
22 txt = tools.indent(txt, pre="")
23 return txt[:-1] if txt.endswith("\n") else txt
25 def __init__(
26 self,
27 message: str,
28 explain: str | None = None,
29 hint: str | None = None,
30 usage: str | None = None,
31 ):
32 self.message = message.strip()
33 self.explain = explain
34 self.hint = hint
35 self.usage = usage
37 def __str__(self):
38 out = []
39 if self.usage:
40 out.extend(self.usage.strip().split("\n"))
41 if self.message:
42 out.extend(self._strip(self.message).split("\n"))
43 if self.explain:
44 out.append("reason:")
45 out.extend(tools.indent(self.explain).split("\n"))
46 if self.hint:
47 out.append("hint:")
48 out.extend(tools.indent(self.hint).split("\n"))
49 return "\n".join((line.strip() if not line.strip() else line) for line in out)
52def _add_arguments(
53 parser: argparse.ArgumentParser,
54) -> None:
55 """parses args from the command line
57 Args:
58 args: command line arguments or None to pull from sys.argv
59 doc: text to use in cli description
60 """
61 parser.add_argument("-n", "--dry-run", dest="dryrun", action="store_true")
62 parser.add_argument("-v", "--verbose", action="store_true")
65def _process_options(
66 options: argparse.Namespace, errorfn: ErrorFn
67) -> argparse.Namespace | None:
68 logging.basicConfig(
69 format="%(levelname)s:%(name)s:(dry-run) %(message)s"
70 if options.dryrun
71 else "%(levelname)s:%(name)s:%(message)s",
72 level=logging.DEBUG if options.verbose else logging.INFO,
73 )
75 for d in [
76 "verbose",
77 ]:
78 delattr(options, d)
79 return options
82def cli(
83 add_arguments: Callable[[argparse.ArgumentParser], None] | None = None,
84 process_options: Callable[[argparse.Namespace, ErrorFn], argparse.Namespace | None]
85 | None = None,
86 doc: str | None = None,
87):
88 @functools.wraps(cli)
89 def _fn(main: Callable[[argparse.Namespace], Any]):
90 @functools.wraps(main)
91 def _fn1(args: None | list[str] = None) -> Any:
92 try:
94 class ParserFormatter(
95 argparse.ArgumentDefaultsHelpFormatter,
96 argparse.RawDescriptionHelpFormatter,
97 ):
98 pass
100 description, _, epilog = (doc or "").partition("\n")
101 parser = argparse.ArgumentParser(
102 formatter_class=ParserFormatter,
103 description=description,
104 epilog=epilog,
105 )
106 _add_arguments(parser)
107 if add_arguments:
108 add_arguments(parser)
110 options = parser.parse_args(args=args)
112 def error(
113 message: str,
114 explain: str = "",
115 hint: str = "",
116 usage: str | None = None,
117 ):
118 raise AbortExecutionError(message, explain, hint, usage)
120 errorfn: ErrorFn = functools.partial(error, usage=parser.format_usage())
121 options.error = errorfn
123 options = _process_options(options, errorfn) or options
124 if process_options:
125 options = process_options(options, errorfn) or options
127 return main(options)
128 except AbortExecutionError as err:
129 print(str(err), file=sys.stderr) # noqa: T201
130 raise SystemExit(2) from None
131 except Exception:
132 raise
134 return _fn1
136 return _fn