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
« 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.
34.. rubric:: Usage
36.. code-block:: bash
38 pyTooling help
39 pyTooling 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 typing import ClassVar, NoReturn
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
58@export
59class Application(TerminalApplication, PipelineHandlers, ArgParseHelperMixin):
60 """
61 The :program:`pyTooling` program: a terminal application whose commands are declared as attributes.
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.
70 def __init__(self) -> None:
71 """Initializes the program, its terminal and its command line parser."""
72 super().__init__(Mode.TextToStdOut_ErrorsToStdErr)
74 textWidth = min(self.Width, 160)
76 class HelpFormatter(RawDescriptionHelpFormatter):
77 """Nested class widening argparse's help page to the terminal, and its option column to 30 characters."""
79 def __init__(self, *args, **kwargs) -> None:
80 """
81 Initializes the formatter with the widths this program wants.
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)
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 )
100 def Run(self, enableAutoComplete: bool = True) -> None:
101 """
102 Parses the command line and dispatches to the handler of the command it names.
104 :param enableAutoComplete: Optional, register the parser with ``argcomplete``, if that package is installed.
105 Default: ``True``.
106 """
107 ArgParseHelperMixin.Run(self, enableAutoComplete)
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()
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``.
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)
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
135 self._PrintHeadline()
136 self._PrintVersion(DunderModule, "pyTooling")
139@export
140def main() -> NoReturn:
141 """
142 Entrypoint to start program execution.
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.
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
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 )
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)
178if __name__ == "__main__": 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true
179 main()