Coverage for src/hatch_ci/cli.py: 86%

# Coverage for src/hatch_ci/cli.py : 86%

## 72 statements 62 run 10 missing 0 excluded

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

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