Coverage for pyTooling/Attributes/ArgParse/__init__.py: 86%
128 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 07:24 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 07:24 +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 """
68 Read-only property to access the handler method (:attr:`_handler`).
70 :returns: The method called to handle the command.
71 """
72 return self._handler
75# FIXME: Is _HandlerMixin needed here, or for commands?
76@export
77class CommandLineArgument(ArgParseAttribute, _HandlerMixin):
78 """
79 Base-class for all *Argument* classes.
81 An argument instance can be converted via ``AsArgument`` to a single string value or a sequence of string values
82 (tuple) usable e.g. with :class:`subprocess.Popen`. Each argument class implements at least one ``pattern`` parameter
83 to specify how argument are formatted.
85 There are multiple derived formats supporting:
87 * commands |br|
88 |rarr| :mod:`~pyTooling.Attribute.ArgParse.Command`
89 * simple names (flags) |br|
90 |rarr| :mod:`~pyTooling.Attribute.ArgParse.Flag`, :mod:`~pyTooling.Attribute.ArgParse.BooleanFlag`
91 * simple values (vlaued flags) |br|
92 |rarr| :class:`~pyTooling.Attribute.ArgParse.Argument.StringArgument`, :class:`~pyTooling.Attribute.ArgParse.Argument.PathArgument`
93 * names and values |br|
94 |rarr| :mod:`~pyTooling.Attribute.ArgParse.ValuedFlag`, :mod:`~pyTooling.Attribute.ArgParse.OptionalValuedFlag`
95 * key-value pairs |br|
96 |rarr| :mod:`~pyTooling.Attribute.ArgParse.NamedKeyValuePair`
97 """
99 # def __init__(self, args: Iterable, kwargs: Mapping) -> None:
100 # """
101 # The constructor expects ``args`` for positional and/or ``kwargs`` for named parameters which are passed without
102 # modification to :meth:`~ArgumentParser.add_argument`.
103 # """
104 #
105 # super().__init__(*args, **kwargs)
107 _args: Tuple
108 _kwargs: Dict
110 def __init__(self, *args: Any, **kwargs: Any) -> None:
111 """
112 The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without
113 modification to :meth:`~ArgumentParser.add_argument`.
114 """
115 super().__init__()
116 self._args = args
117 self._kwargs = kwargs
119 @readonly
120 def Args(self) -> Tuple:
121 """
122 A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are
123 passed without modification to :class:`~ArgumentParser`.
125 :returns: Tuple of positional parameters.
126 """
127 return self._args
129 @readonly
130 def KWArgs(self) -> Dict:
131 """
132 A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are
133 passed without modification to :class:`~ArgumentParser`.
135 :returns: Dictionary of named parameters.
136 """
137 return self._kwargs
140@export
141class CommandGroupAttribute(ArgParseAttribute):
142 """
143 *Experimental* attribute to group sub-commands in groups for better readability in a ``prog.py --help`` call.
144 """
145 __groupName: str = None
147 def __init__(self, groupName: str) -> None:
148 """
149 The constructor expects a 'groupName' which can be used to group sub-commands for better readability.
150 """
151 super().__init__()
152 self.__groupName = groupName
154 @readonly
155 def GroupName(self) -> str:
156 """
157 Read-only property to access the name of the command group (:attr:`_groupName`).
159 :returns: Name of the command group.
160 """
161 return self.__groupName
164# @export
165# class _KwArgsMixin(metaclass=ExtendedType, mixin=True):
166# """
167# A mixin-class that offers a class field for named parameters (```**kwargs``) and a matching property.
168# """
169# _kwargs: Dict #: A dictionary of additional keyword parameters.
170#
171# @readonly
172# def KWArgs(self) -> Dict:
173# """
174# A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are
175# passed without modification to :class:`~ArgumentParser`.
176# """
177# return self._kwargs
178#
179#
180# @export
181# class _ArgsMixin(_KwArgsMixin, mixin=True):
182# """
183# A mixin-class that offers a class field for positional parameters (```*args``) and a matching property.
184# """
185#
186# _args: Tuple #: A tuple of additional positional parameters.
187#
188# @readonly
189# def Args(self) -> Tuple:
190# """
191# A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are
192# passed without modification to :class:`~ArgumentParser`.
193# """
194# return self._args
197@export
198class DefaultHandler(ArgParseAttribute, _HandlerMixin):
199 """
200 Marks a handler method as *default* handler. This method is called if no sub-command is given.
202 It's an error, if more than one method is annotated with this attribute.
203 """
205 def __call__(self, func: Callable) -> Callable:
206 self._handler = func
207 return super().__call__(func)
210@export
211class CommandHandler(ArgParseAttribute, _HandlerMixin): #, _KwArgsMixin):
212 """Marks a handler method as responsible for the given 'command'. This constructs
213 a sub-command parser using :meth:`~ArgumentParser.add_subparsers`.
214 """
216 _command: str
217 _help: str
218 # FIXME: extract to mixin?
219 _args: Tuple
220 _kwargs: Dict
222 def __init__(self, command: str, help: str = "", **kwargs: Any) -> None:
223 """The constructor expects a 'command' and an optional list of named parameters
224 (keyword arguments) which are passed without modification to :meth:`~ArgumentParser.add_subparsers`.
225 """
226 super().__init__()
227 self._command = command
228 self._help = help
229 self._args = tuple()
230 self._kwargs = kwargs
232 self._kwargs["help"] = help
234 def __call__(self, func: M) -> M:
235 self._handler = func
236 return super().__call__(func)
238 @readonly
239 def Command(self) -> str:
240 """
241 Read-only property to access the command a sub-command parser adheres to (:attr:`_command`).
243 :returns: Name of the command.
244 """
245 return self._command
247# FIXME: extract to mixin?
248 @readonly
249 def Args(self) -> Tuple:
250 """
251 A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are
252 passed without modification to :class:`~ArgumentParser`.
254 :returns: Tuple of positional parameters.
255 """
256 return self._args
258 # FIXME: extract to mixin?
259 @readonly
260 def KWArgs(self) -> Dict:
261 """
262 A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are
263 passed without modification to :class:`~ArgumentParser`.
265 :returns: Dictionary of named parameters.
266 """
267 return self._kwargs
270@export
271class ArgParseHelperMixin(metaclass=ExtendedType, mixin=True):
272 """
273 Mixin-class to implement an :mod:`argparse`-base command line argument processor.
274 """
275 _mainParser: ArgumentParser
276 _formatter: Any # TODO: Find type
277 _subParser: Any # TODO: Find type
278 _subParsers: Dict[str, ArgumentParser]
280 def __init__(self, **kwargs: Any) -> None:
281 """
282 The mixin-constructor expects an optional list of named parameters which are passed without modification to the
283 :class:`ArgumentParser` constructor.
284 """
285 from .Argument import CommandLineArgument
287 super().__init__()
289 self._subParser = None
290 self._subParsers = {}
291 self._formatter = kwargs["formatter_class"] if "formatter_class" in kwargs else None
293 if "formatter_class" in kwargs: 293 ↛ 294line 293 didn't jump to line 294 because the condition on line 293 was never true
294 self._formatter = kwargs["formatter_class"]
295 if "allow_abbrev" not in kwargs: 295 ↛ 297line 295 didn't jump to line 297 because the condition on line 295 was always true
296 kwargs["allow_abbrev"] = False
297 if "exit_on_error" not in kwargs: 297 ↛ 301line 297 didn't jump to line 301 because the condition on line 297 was always true
298 kwargs["exit_on_error"] = False
300 # create a commandline argument parser
301 self._mainParser = ArgumentParser(**kwargs)
303 # Search for 'DefaultHandler' marked method
304 methods = self.GetMethodsWithAttributes(predicate=DefaultHandler)
305 if (methodCount := len(methods)) == 1: 305 ↛ 318line 305 didn't jump to line 318 because the condition on line 305 was always true
306 defaultMethod, attributes = firstPair(methods)
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 default handler multiple times with 'DefaultAttribute'.")
310 # set default handler for the main parser
311 self._mainParser.set_defaults(func=firstElement(attributes).Handler)
313 # Add argument descriptions for the main parser
314 methodAttributes = defaultMethod.GetAttributes(CommandLineArgument) # ArgumentAttribute)
315 for methodAttribute in methodAttributes:
316 self._mainParser.add_argument(*methodAttribute.Args, **methodAttribute.KWArgs)
318 elif methodCount > 1:
319 raise ArgParseException("Marked more then one handler as default handler with 'DefaultAttribute'.")
321 # Search for 'CommandHandler' marked methods
322 methods: Dict[Callable, Tuple[CommandHandler]] = self.GetMethodsWithAttributes(predicate=CommandHandler)
323 for method, attributes in methods.items():
324 if self._subParser is None:
325 self._subParser = self._mainParser.add_subparsers(help='sub-command help')
327 if len(attributes) > 1: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true
328 raise ArgParseException("Marked command handler multiple times with 'CommandHandler'.")
330 # Add a sub parser for each command / handler pair
331 attribute = firstElement(attributes)
332 kwArgs = attribute.KWArgs.copy()
333 if "formatter_class" not in kwArgs and self._formatter is not None: 333 ↛ 334line 333 didn't jump to line 334 because the condition on line 333 was never true
334 kwArgs["formatter_class"] = self._formatter
336 kwArgs["allow_abbrev"] = False if "allow_abbrev" not in kwargs else kwargs["allow_abbrev"]
338 subParser = self._subParser.add_parser(attribute.Command, **kwArgs)
339 subParser.set_defaults(func=attribute.Handler)
341 # Add arguments for the sub-parsers
342 methodAttributes = method.GetAttributes(CommandLineArgument) # ArgumentAttribute)
343 for methodAttribute in methodAttributes:
344 subParser.add_argument(*methodAttribute.Args, **methodAttribute.KWArgs)
346 self._subParsers[attribute.Command] = subParser
348 def Run(self, enableAutoComplete: bool = True) -> None:
349 if enableAutoComplete: 349 ↛ 352line 349 didn't jump to line 352 because the condition on line 349 was always true
350 self._EnabledAutoComplete()
352 self._ParseArguments()
354 def _EnabledAutoComplete(self) -> None:
355 try:
356 from argcomplete import autocomplete
357 autocomplete(self._mainParser)
358 except ImportError: # pragma: no cover
359 pass
361 def _ParseArguments(self) -> None:
362 # parse command line options and process split arguments in callback functions
363 parsed, args = self._mainParser.parse_known_args()
364 self._RouteToHandler(parsed)
366 def _RouteToHandler(self, args: Namespace) -> None:
367 # because func is a function (unbound to an object), it MUST be called with self as a first parameter
368 args.func(self, args)
370 @readonly
371 def MainParser(self) -> ArgumentParser:
372 """
373 Read-only property to access the main argument parser (:attr:`_mainParser`).
375 :returns: The main argument parser.
376 """
377 return self._mainParser
379 @readonly
380 def SubParsers(self) -> Dict[str, ArgumentParser]:
381 """
382 Read-only property to access the sub-parsers (:attr:`_subParser`).
384 :returns: Dictionary of command names and their sub-parsers.
385 """
386 return self._subParsers
389# String
390# StringList
391# Path
392# PathList
393# Delimiter
394# ValuedFlag --option=value
395# ValuedFlagList --option=foo --option=bar
396# OptionalValued --option --option=foo
397# ValuedTuple