Coverage for pyTooling / Attributes / ArgParse / __init__.py: 87%
138 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-13 22:36 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-13 22:36 +0000
1# ==================================================================================================================== #
2# _ _ _ _ _ _ _ ____ #
3# / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ #
4# / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ #
5# _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ #
6# (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| #
7# |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany #
15# Copyright 2007-2016 Patrick Lehmann - Dresden, Germany #
16# #
17# Licensed under the Apache License, Version 2.0 (the "License"); #
18# you may not use this file except in compliance with the License. #
19# You may obtain a copy of the License at #
20# #
21# http://www.apache.org/licenses/LICENSE-2.0 #
22# #
23# Unless required by applicable law or agreed to in writing, software #
24# distributed under the License is distributed on an "AS IS" BASIS, #
25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
26# See the License for the specific language governing permissions and #
27# limitations under the License. #
28# #
29# SPDX-License-Identifier: Apache-2.0 #
30# ==================================================================================================================== #
31#
32from argparse import ArgumentParser, Namespace
33from typing import Callable, Dict, Tuple, Any, TypeVar
35from pyTooling.Decorators import export, readonly
36from pyTooling.MetaClasses import ExtendedType
37from pyTooling.Exceptions import ToolingException
38from pyTooling.Common import firstElement, firstPair
39from pyTooling.Attributes import Attribute
42M = TypeVar("M", bound=Callable)
45@export
46class ArgParseException(ToolingException):
47 pass
50#@abstract
51@export
52class ArgParseAttribute(Attribute):
53 """
54 Base-class for all attributes to describe a :mod:`argparse`-base command line argument parser.
55 """
58@export
59class _HandlerMixin(metaclass=ExtendedType, mixin=True):
60 """
61 A mixin-class that offers a class field for a reference to a handler method and a matching property.
62 """
63 _handler: Callable = None #: Reference to a method that is called to handle e.g. a sub-command.
65 @readonly
66 def Handler(self) -> Callable:
67 """Returns the handler method."""
68 return self._handler
71# FIXME: Is _HandlerMixin needed here, or for commands?
72@export
73class CommandLineArgument(ArgParseAttribute, _HandlerMixin):
74 """
75 Base-class for all *Argument* classes.
77 An argument instance can be converted via ``AsArgument`` to a single string value or a sequence of string values
78 (tuple) usable e.g. with :class:`subprocess.Popen`. Each argument class implements at least one ``pattern`` parameter
79 to specify how argument are formatted.
81 There are multiple derived formats supporting:
83 * commands |br|
84 |rarr| :mod:`~pyTooling.Attribute.ArgParse.Command`
85 * simple names (flags) |br|
86 |rarr| :mod:`~pyTooling.Attribute.ArgParse.Flag`, :mod:`~pyTooling.Attribute.ArgParse.BooleanFlag`
87 * simple values (vlaued flags) |br|
88 |rarr| :class:`~pyTooling.Attribute.ArgParse.Argument.StringArgument`, :class:`~pyTooling.Attribute.ArgParse.Argument.PathArgument`
89 * names and values |br|
90 |rarr| :mod:`~pyTooling.Attribute.ArgParse.ValuedFlag`, :mod:`~pyTooling.Attribute.ArgParse.OptionalValuedFlag`
91 * key-value pairs |br|
92 |rarr| :mod:`~pyTooling.Attribute.ArgParse.NamedKeyValuePair`
93 """
95 # def __init__(self, args: Iterable, kwargs: Mapping) -> None:
96 # """
97 # The constructor expects ``args`` for positional and/or ``kwargs`` for named parameters which are passed without
98 # modification to :meth:`~ArgumentParser.add_argument`.
99 # """
100 #
101 # super().__init__(*args, **kwargs)
103 _args: Tuple
104 _kwargs: Dict
106 def __init__(self, *args: Any, **kwargs: Any) -> None:
107 """
108 The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without
109 modification to :meth:`~ArgumentParser.add_argument`.
110 """
111 super().__init__()
112 self._args = args
113 self._kwargs = kwargs
115 @readonly
116 def Args(self) -> Tuple:
117 """
118 A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are
119 passed without modification to :class:`~ArgumentParser`.
120 """
121 return self._args
123 @readonly
124 def KWArgs(self) -> Dict:
125 """
126 A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are
127 passed without modification to :class:`~ArgumentParser`.
128 """
129 return self._kwargs
132@export
133class CommandGroupAttribute(ArgParseAttribute):
134 """
135 *Experimental* attribute to group sub-commands in groups for better readability in a ``prog.py --help`` call.
136 """
137 __groupName: str = None
139 def __init__(self, groupName: str) -> None:
140 """
141 The constructor expects a 'groupName' which can be used to group sub-commands for better readability.
142 """
143 super().__init__()
144 self.__groupName = groupName
146 @readonly
147 def GroupName(self) -> str:
148 """Returns the name of the command group."""
149 return self.__groupName
152# @export
153# class _KwArgsMixin(metaclass=ExtendedType, mixin=True):
154# """
155# A mixin-class that offers a class field for named parameters (```**kwargs``) and a matching property.
156# """
157# _kwargs: Dict #: A dictionary of additional keyword parameters.
158#
159# @readonly
160# def KWArgs(self) -> Dict:
161# """
162# A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are
163# passed without modification to :class:`~ArgumentParser`.
164# """
165# return self._kwargs
166#
167#
168# @export
169# class _ArgsMixin(_KwArgsMixin, mixin=True):
170# """
171# A mixin-class that offers a class field for positional parameters (```*args``) and a matching property.
172# """
173#
174# _args: Tuple #: A tuple of additional positional parameters.
175#
176# @readonly
177# def Args(self) -> Tuple:
178# """
179# A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are
180# passed without modification to :class:`~ArgumentParser`.
181# """
182# return self._args
185@export
186class DefaultHandler(ArgParseAttribute, _HandlerMixin):
187 """
188 Marks a handler method as *default* handler. This method is called if no sub-command is given.
190 It's an error, if more than one method is annotated with this attribute.
191 """
193 def __call__(self, func: Callable) -> Callable:
194 self._handler = func
195 return super().__call__(func)
198@export
199class CommandHandler(ArgParseAttribute, _HandlerMixin): #, _KwArgsMixin):
200 """Marks a handler method as responsible for the given 'command'. This constructs
201 a sub-command parser using :meth:`~ArgumentParser.add_subparsers`.
202 """
204 _command: str
205 _help: str
206 # FIXME: extract to mixin?
207 _args: Tuple
208 _kwargs: Dict
210 def __init__(self, command: str, help: str = "", **kwargs: Any) -> None:
211 """The constructor expects a 'command' and an optional list of named parameters
212 (keyword arguments) which are passed without modification to :meth:`~ArgumentParser.add_subparsers`.
213 """
214 super().__init__()
215 self._command = command
216 self._help = help
217 self._args = tuple()
218 self._kwargs = kwargs
220 self._kwargs["help"] = help
222 def __call__(self, func: M) -> M:
223 self._handler = func
224 return super().__call__(func)
226 @readonly
227 def Command(self) -> str:
228 """Returns the 'command' a sub-command parser adheres to."""
229 return self._command
231# FIXME: extract to mixin?
232 @readonly
233 def Args(self) -> Tuple:
234 """
235 A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are
236 passed without modification to :class:`~ArgumentParser`.
237 """
238 return self._args
240 # FIXME: extract to mixin?
241 @readonly
242 def KWArgs(self) -> Dict:
243 """
244 A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are
245 passed without modification to :class:`~ArgumentParser`.
246 """
247 return self._kwargs
250@export
251class ArgParseHelperMixin(metaclass=ExtendedType, mixin=True):
252 """
253 Mixin-class to implement an :mod:`argparse`-base command line argument processor.
254 """
255 _mainParser: ArgumentParser
256 _formatter: Any # TODO: Find type
257 _subParser: Any # TODO: Find type
258 _subParsers: Dict # TODO: Find type
260 def __init__(self, **kwargs: Any) -> None:
261 """
262 The mixin-constructor expects an optional list of named parameters which are passed without modification to the
263 :class:`ArgumentParser` constructor.
264 """
265 from .Argument import CommandLineArgument
267 super().__init__()
269 self._subParser = None
270 self._subParsers = {}
271 self._formatter = kwargs["formatter_class"] if "formatter_class" in kwargs else None
273 if "formatter_class" in kwargs: 273 ↛ 274line 273 didn't jump to line 274 because the condition on line 273 was never true
274 self._formatter = kwargs["formatter_class"]
275 if "allow_abbrev" not in kwargs: 275 ↛ 277line 275 didn't jump to line 277 because the condition on line 275 was always true
276 kwargs["allow_abbrev"] = False
277 if "exit_on_error" not in kwargs: 277 ↛ 281line 277 didn't jump to line 281 because the condition on line 277 was always true
278 kwargs["exit_on_error"] = False
280 # create a commandline argument parser
281 self._mainParser = ArgumentParser(**kwargs)
283 # Search for 'DefaultHandler' marked method
284 methods = self.GetMethodsWithAttributes(predicate=DefaultHandler)
285 if (methodCount := len(methods)) == 1: 285 ↛ 298line 285 didn't jump to line 298 because the condition on line 285 was always true
286 defaultMethod, attributes = firstPair(methods)
287 if len(attributes) > 1: 287 ↛ 288line 287 didn't jump to line 288 because the condition on line 287 was never true
288 raise ArgParseException("Marked default handler multiple times with 'DefaultAttribute'.")
290 # set default handler for the main parser
291 self._mainParser.set_defaults(func=firstElement(attributes).Handler)
293 # Add argument descriptions for the main parser
294 methodAttributes = defaultMethod.GetAttributes(CommandLineArgument) # ArgumentAttribute)
295 for methodAttribute in methodAttributes:
296 self._mainParser.add_argument(*methodAttribute.Args, **methodAttribute.KWArgs)
298 elif methodCount > 1:
299 raise ArgParseException("Marked more then one handler as default handler with 'DefaultAttribute'.")
301 # Search for 'CommandHandler' marked methods
302 methods: Dict[Callable, Tuple[CommandHandler]] = self.GetMethodsWithAttributes(predicate=CommandHandler)
303 for method, attributes in methods.items():
304 if self._subParser is None:
305 self._subParser = self._mainParser.add_subparsers(help='sub-command help')
307 if len(attributes) > 1: 307 ↛ 308line 307 didn't jump to line 308 because the condition on line 307 was never true
308 raise ArgParseException("Marked command handler multiple times with 'CommandHandler'.")
310 # Add a sub parser for each command / handler pair
311 attribute = firstElement(attributes)
312 kwArgs = attribute.KWArgs.copy()
313 if "formatter_class" not in kwArgs and self._formatter is not None: 313 ↛ 314line 313 didn't jump to line 314 because the condition on line 313 was never true
314 kwArgs["formatter_class"] = self._formatter
316 kwArgs["allow_abbrev"] = False if "allow_abbrev" not in kwargs else kwargs["allow_abbrev"]
318 subParser = self._subParser.add_parser(attribute.Command, **kwArgs)
319 subParser.set_defaults(func=attribute.Handler)
321 # Add arguments for the sub-parsers
322 methodAttributes = method.GetAttributes(CommandLineArgument) # ArgumentAttribute)
323 for methodAttribute in methodAttributes:
324 subParser.add_argument(*methodAttribute.Args, **methodAttribute.KWArgs)
326 self._subParsers[attribute.Command] = subParser
328 def Run(self, enableAutoComplete: bool = True) -> None:
329 if enableAutoComplete: 329 ↛ 332line 329 didn't jump to line 332 because the condition on line 329 was always true
330 self._EnabledAutoComplete()
332 self._ParseArguments()
334 def _EnabledAutoComplete(self) -> None:
335 try:
336 from argcomplete import autocomplete
337 autocomplete(self._mainParser)
338 except ImportError: # pragma: no cover
339 pass
341 def _ParseArguments(self) -> None:
342 # parse command line options and process split arguments in callback functions
343 parsed, args = self._mainParser.parse_known_args()
344 self._RouteToHandler(parsed)
346 def _RouteToHandler(self, args: Namespace) -> None:
347 # because func is a function (unbound to an object), it MUST be called with self as a first parameter
348 args.func(self, args)
350 @readonly
351 def MainParser(self) -> ArgumentParser:
352 """Returns the main parser."""
353 return self._mainParser
355 @readonly
356 def SubParsers(self) -> Dict:
357 """Returns the sub-parsers."""
358 return self._subParsers
361# String
362# StringList
363# Path
364# PathList
365# Delimiter
366# ValuedFlag --option=value
367# ValuedFlagList --option=foo --option=bar
368# OptionalValued --option --option=foo
369# ValuedTuple