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

72 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-08-07 05:56 +0000

1from __future__ import annotations 

2 

3import argparse 

4import functools 

5import logging 

6import sys 

7from typing import Any, Callable, Protocol 

8 

9from . import tools 

10 

11 

12class ErrorFn(Protocol): 

13 def __call__(self, message: str, explain: str | None, hint: str | None) -> None: 

14 ... 

15 

16 

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 

24 

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 

36 

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) 

50 

51 

52def _add_arguments( 

53 parser: argparse.ArgumentParser, 

54) -> None: 

55 """parses args from the command line 

56 

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") 

63 

64 

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 ) 

74 

75 for d in [ 

76 "verbose", 

77 ]: 

78 delattr(options, d) 

79 return options 

80 

81 

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: 

93 

94 class ParserFormatter( 

95 argparse.ArgumentDefaultsHelpFormatter, 

96 argparse.RawDescriptionHelpFormatter, 

97 ): 

98 pass 

99 

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) 

109 

110 options = parser.parse_args(args=args) 

111 

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) 

119 

120 errorfn: ErrorFn = functools.partial(error, usage=parser.format_usage()) 

121 options.error = errorfn 

122 

123 options = _process_options(options, errorfn) or options 

124 if process_options: 

125 options = process_options(options, errorfn) or options 

126 

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 

133 

134 return _fn1 

135 

136 return _fn