Coverage for pyTooling/GitHub/CLI/__init__.py: 74%

67 statements  

« prev     ^ index     » next       coverage.py v7.17.3, created at 2026-11-14 02:08 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ _ _ _ _ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___(_) |_| | | |_ _| |__ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| | __| |_| | | | | '_ \ # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | |_| _ | |_| | |_) | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_|\__|_| |_|\__,_|_.__/ # 

7# |_| |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2026-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32The command line interface of pyTooling.GitHub, and its :program:`pytooling-github` program. 

33 

34.. rubric:: Usage 

35 

36.. code-block:: bash 

37 

38 pytooling-github help 

39 pytooling-github version 

40 

41.. hint:: 

42 

43 See :ref:`high-level help <CLI>` for explanations and usage examples. 

44""" 

45from argparse import RawDescriptionHelpFormatter, Namespace 

46from textwrap import dedent 

47from types import ModuleType 

48from typing import ClassVar, NoReturn 

49 

50from pyTooling import GitHub 

51from pyTooling.Decorators import export 

52from pyTooling.Exceptions import ExceptionBase, MissingDependencyError, ToolingException 

53from pyTooling.Attributes.ArgParse import ArgParseHelperMixin, DefaultHandler, CommandHandler 

54from pyTooling.Attributes.ArgParse.Flag import FlagArgument 

55from pyTooling.Attributes.ArgParse.Argument import StringArgument 

56from pyTooling.TerminalUI import TerminalApplication, Mode 

57from pyTooling.GitHub.CLI.Pipeline import PipelineHandlers 

58 

59 

60@export 

61class Application(TerminalApplication, PipelineHandlers, ArgParseHelperMixin): 

62 """ 

63 The :program:`pytooling-github` program: a terminal application whose commands are declared as attributes. 

64 

65 Every command is a method marked with :class:`~pyTooling.Attributes.ArgParse.CommandHandler`, and the arguments 

66 of a command are the attributes written on that method. A group of related commands is a mixin-class of its own, 

67 which this class inherits from - so a new command is a new mixin and one more base-class, and nothing here has 

68 to know what it does. 

69 """ 

70 HEADLINE: ClassVar[str] = "pyTooling.GitHub Service Program" #: Headline above every command's output. 

71 DUNDER_MODULE: ClassVar[ModuleType] = GitHub #: Package with the dunder variables. 

72 

73 def __init__(self) -> None: 

74 """Initializes the program, its terminal and its command line parser.""" 

75 super().__init__(Mode.TextToStdOut_ErrorsToStdErr) 

76 

77 textWidth = min(self.Width, 160) 

78 

79 class HelpFormatter(RawDescriptionHelpFormatter): 

80 """Nested class widening argparse's help page to the terminal, and its option column to 30 characters.""" 

81 

82 def __init__(self, *args, **kwargs) -> None: 

83 """ 

84 Initializes the formatter with the widths this program wants. 

85 

86 :param args: Positional parameters, passed on to :class:`~argparse.RawDescriptionHelpFormatter`. 

87 :param kwargs: Keyword parameters, passed on after the two widths were set. 

88 """ 

89 kwargs["max_help_position"] = 30 

90 kwargs["width"] = textWidth 

91 super().__init__(*args, **kwargs) 

92 

93 ArgParseHelperMixin.__init__( 

94 self, 

95 prog="pytooling-github", 

96 description=dedent(f"""\ 

97 '{self.HEADLINE}' to work with GitHub pipelines. 

98 """), 

99 formatter_class=HelpFormatter, 

100 add_help=False 

101 ) 

102 

103 def Run(self, enableAutoComplete: bool = True) -> None: 

104 """ 

105 Parses the command line and dispatches to the handler of the command it names. 

106 

107 :param enableAutoComplete: Optional, register the parser with ``argcomplete``, if that package is installed. 

108 Default: ``True``. 

109 """ 

110 ArgParseHelperMixin.Run(self, enableAutoComplete) 

111 

112 @DefaultHandler() 

113 @FlagArgument("-q", "--quiet", dest="quiet", help="Reduce messages to a minimum.") 

114 @FlagArgument("-v", "--verbose", dest="verbose", help="Print out detailed messages.") 

115 @FlagArgument("-d", "--debug", dest="debug", help="Enable debug mode.") 

116 def HandleDefault(self, _: Namespace) -> None: 

117 """Handle program calls without any command.""" 

118 self._PrintHeadline() 

119 self._PrintHelp() 

120 

121 @CommandHandler("help", help="Display help page(s) for the given command name.", 

122 description="Display help page(s) for the given command name.") 

123 @StringArgument(dest="Command", metaName="Command", optional=True, help="Print help page(s) for a command.") 

124 def HandleHelp(self, args: Namespace) -> None: 

125 """ 

126 Handle program calls with command ``help``. 

127 

128 :param args: The parsed command line, whose ``Command`` names the command to print the help page of. 

129 """ 

130 self._PrintHeadline() 

131 self._PrintHelp(args.Command) 

132 

133 @CommandHandler("version", help="Display version information.", description="Display version information.") 

134 def HandleVersion(self, _: Namespace) -> None: 

135 """Handle program calls with command ``version``.""" 

136 self._PrintHeadline() 

137 self._PrintVersion("pyTooling.GitHub") 

138 

139 

140@export 

141def main() -> NoReturn: 

142 """ 

143 Entrypoint to start program execution. 

144 

145 This function is called either from :pycode:`if __name__ == "__main__":` or through the ``console_scripts`` 

146 entry point :program:`pytooling-github`, which :file:`setup.py` registers. 

147 

148 It creates the :class:`Application` and runs it in a ``try ... except`` environment, so an exception is reported 

149 as a message and a non-zero exit code rather than as a traceback. 

150 """ 

151 from sys import argv 

152 

153 program = Application() 

154 program.Configure( 

155 verbose=("-v" in argv or "--verbose" in argv), 

156 debug=("-d" in argv or "--debug" in argv), 

157 silent=("-q" in argv or "--quiet" in argv) 

158 ) 

159 

160 try: 

161 program.Run() 

162 except MissingDependencyError as ex: 

163 program.PrintMissingDependencyError(ex) 

164 except ToolingException as ex: 

165 program.WriteLineToStdErr(f"{{RED}}[ERROR] {ex}{{NOCOLOR}}".format(**program.FOREGROUND)) 

166 if ex.__cause__ is not None: 

167 program.WriteLineToStdErr(f"{{DARK_YELLOW}}Because of: {ex.__cause__}{{NOCOLOR}}".format(**program.FOREGROUND)) 

168 

169 for note in getattr(ex, "__notes__", ()) or (): 

170 program.WriteLineToStdErr(f"{{DARK_YELLOW}} [NOTE] {note}{{NOCOLOR}}".format(**program.FOREGROUND)) 

171 program.Exit(1) 

172 except ExceptionBase as ex: 

173 program.PrintExceptionBase(ex) 

174 except NotImplementedError as ex: 

175 program.PrintNotImplementedError(ex) 

176 except Exception as ex: 

177 program.PrintException(ex) 

178 

179 

180if __name__ == "__main__": 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true

181 main()