Coverage for pyTooling/CLI/__init__.py: 73%

65 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-09-29 23:34 +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, and its :program:`pyTooling` program. 

33 

34.. rubric:: Usage 

35 

36.. code-block:: bash 

37 

38 pyTooling help 

39 pyTooling 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 typing import ClassVar, NoReturn 

48 

49from pyTooling.Decorators import export 

50from pyTooling.Exceptions import ExceptionBase, MissingDependencyError, ToolingException 

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

52from pyTooling.Attributes.ArgParse.Flag import FlagArgument 

53from pyTooling.Attributes.ArgParse.Argument import StringArgument 

54from pyTooling.TerminalUI import TerminalApplication, Mode 

55from pyTooling.CLI.Pipeline import PipelineHandlers 

56 

57 

58@export 

59class Application(TerminalApplication, PipelineHandlers, ArgParseHelperMixin): 

60 """ 

61 The :program:`pyTooling` program: a terminal application whose commands are declared as attributes. 

62 

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

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

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

66 to know what it does. 

67 """ 

68 HeadLine: ClassVar[str] = "pyTooling Service Program" #: Headline printed above every command's output. 

69 

70 def __init__(self) -> None: 

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

72 super().__init__(Mode.TextToStdOut_ErrorsToStdErr) 

73 

74 textWidth = min(self.Width, 160) 

75 

76 class HelpFormatter(RawDescriptionHelpFormatter): 

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

78 

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

80 """ 

81 Initializes the formatter with the widths this program wants. 

82 

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

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

85 """ 

86 kwargs["max_help_position"] = 30 

87 kwargs["width"] = textWidth 

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

89 

90 ArgParseHelperMixin.__init__( 

91 self, 

92 prog="pyTooling", 

93 description=dedent(f"""\ 

94 '{self.HeadLine}' to work with pyTooling data models. 

95 """), 

96 formatter_class=HelpFormatter, 

97 add_help=False 

98 ) 

99 

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

101 """ 

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

103 

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

105 Default: ``True``. 

106 """ 

107 ArgParseHelperMixin.Run(self, enableAutoComplete) 

108 

109 @DefaultHandler() 

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

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

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

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

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

115 self._PrintHeadline() 

116 self._PrintHelp() 

117 

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

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

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

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

122 """ 

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

124 

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

126 """ 

127 self._PrintHeadline() 

128 self._PrintHelp(args.Command) 

129 

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

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

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

133 from pyTooling import Common as DunderModule 

134 

135 self._PrintHeadline() 

136 self._PrintVersion(DunderModule, "pyTooling") 

137 

138 

139@export 

140def main() -> NoReturn: 

141 """ 

142 Entrypoint to start program execution. 

143 

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

145 entry point :program:`pyTooling`, which :file:`setup.py` registers. 

146 

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

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

149 """ 

150 from sys import argv 

151 

152 program = Application() 

153 program.Configure( 

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

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

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

157 ) 

158 

159 try: 

160 program.Run() 

161 except MissingDependencyError as ex: 

162 program.PrintMissingDependencyError(ex) 

163 except ToolingException as ex: 

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

165 if ex.__cause__ is not None: 

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

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

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

169 program.Exit(1) 

170 except ExceptionBase as ex: 

171 program.PrintExceptionBase(ex) 

172 except NotImplementedError as ex: 

173 program.PrintNotImplementedError(ex) 

174 except Exception as ex: 

175 program.PrintException(ex) 

176 

177 

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

179 main()