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
« 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.
34.. rubric:: Usage
36.. code-block:: bash
38 pytooling-github help
39 pytooling-github version
41.. hint::
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
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
60@export
61class Application(TerminalApplication, PipelineHandlers, ArgParseHelperMixin):
62 """
63 The :program:`pytooling-github` program: a terminal application whose commands are declared as attributes.
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.
73 def __init__(self) -> None:
74 """Initializes the program, its terminal and its command line parser."""
75 super().__init__(Mode.TextToStdOut_ErrorsToStdErr)
77 textWidth = min(self.Width, 160)
79 class HelpFormatter(RawDescriptionHelpFormatter):
80 """Nested class widening argparse's help page to the terminal, and its option column to 30 characters."""
82 def __init__(self, *args, **kwargs) -> None:
83 """
84 Initializes the formatter with the widths this program wants.
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)
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 )
103 def Run(self, enableAutoComplete: bool = True) -> None:
104 """
105 Parses the command line and dispatches to the handler of the command it names.
107 :param enableAutoComplete: Optional, register the parser with ``argcomplete``, if that package is installed.
108 Default: ``True``.
109 """
110 ArgParseHelperMixin.Run(self, enableAutoComplete)
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()
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``.
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)
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")
140@export
141def main() -> NoReturn:
142 """
143 Entrypoint to start program execution.
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.
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
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 )
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))
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)
180if __name__ == "__main__": 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true
181 main()