Coverage for pyTooling/Attributes/ArgParse/__init__.py: 87%
139 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-18 22:20 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-18 22:20 +0000
1# ==================================================================================================================== #
2# _ _ _ _ _ _ _ ____ #
3# / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ / \ _ __ __ _| _ \ __ _ _ __ ___ ___ #
4# / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| / _ \ | '__/ _` | |_) / _` | '__/ __|/ _ \ #
5# _ _ _ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \_ / ___ \| | | (_| | __/ (_| | | \__ \ __/ #
6# (_|_|_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___(_)_/ \_\_| \__, |_| \__,_|_| |___/\___| #
7# |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2017-2025 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
35try:
36 from pyTooling.Decorators import export, readonly
37 from pyTooling.MetaClasses import ExtendedType
38 from pyTooling.Exceptions import ToolingException
39 from pyTooling.Common import firstElement, firstPair
40 from pyTooling.Attributes import Attribute
41except (ImportError, ModuleNotFoundError): # pragma: no cover
42 print("[pyTooling.Attributes.ArgParse] Could not import from 'pyTooling.*'!")
44 try:
45 from Decorators import export, readonly
46 from MetaClasses import ExtendedType
47 from Exceptions import ToolingException
48 from Common import firstElement, firstPair
49 from Attributes import Attribute
50 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
51 print("[pyTooling.Attributes.ArgParse] Could not import directly!")
52 raise ex
55M = TypeVar("M", bound=Callable)
58@export
59class ArgParseException(ToolingException):
60 pass
63#@abstract
64@export
65class ArgParseAttribute(Attribute):
66 """
67 Base-class for all attributes to describe a :mod:`argparse`-base command line argument parser.
68 """
71@export
72class _HandlerMixin(metaclass=ExtendedType, mixin=True):
73 """
74 A mixin-class that offers a class field for a reference to a handler method and a matching property.
75 """
76 _handler: Callable = None #: Reference to a method that is called to handle e.g. a sub-command.
78 @readonly
79 def Handler(self) -> Callable:
80 """Returns the handler method."""
81 return self._handler
84# FIXME: Is _HandlerMixin needed here, or for commands?
85@export
86class CommandLineArgument(ArgParseAttribute, _HandlerMixin):
87 """
88 Base-class for all *Argument* classes.
90 An argument instance can be converted via ``AsArgument`` to a single string value or a sequence of string values
91 (tuple) usable e.g. with :class:`subprocess.Popen`. Each argument class implements at least one ``pattern`` parameter
92 to specify how argument are formatted.
94 There are multiple derived formats supporting:
96 * commands |br|
97 |rarr| :mod:`~pyTooling.Attribute.ArgParse.Command`
98 * simple names (flags) |br|
99 |rarr| :mod:`~pyTooling.Attribute.ArgParse.Flag`, :mod:`~pyTooling.Attribute.ArgParse.BooleanFlag`
100 * simple values (vlaued flags) |br|
101 |rarr| :class:`~pyTooling.Attribute.ArgParse.Argument.StringArgument`, :class:`~pyTooling.Attribute.ArgParse.Argument.PathArgument`
102 * names and values |br|
103 |rarr| :mod:`~pyTooling.Attribute.ArgParse.ValuedFlag`, :mod:`~pyTooling.Attribute.ArgParse.OptionalValuedFlag`
104 * key-value pairs |br|
105 |rarr| :mod:`~pyTooling.Attribute.ArgParse.NamedKeyValuePair`
106 """
108 # def __init__(self, args: Iterable, kwargs: Mapping) -> None:
109 # """
110 # The constructor expects ``args`` for positional and/or ``kwargs`` for named parameters which are passed without
111 # modification to :meth:`~ArgumentParser.add_argument`.
112 # """
113 #
114 # super().__init__(*args, **kwargs)
116 _args: Tuple
117 _kwargs: Dict
119 def __init__(self, *args: Any, **kwargs: Any) -> None:
120 """
121 The constructor expects positional (``*args``) and/or named parameters (``**kwargs``) which are passed without
122 modification to :meth:`~ArgumentParser.add_argument`.
123 """
124 super().__init__()
125 self._args = args
126 self._kwargs = kwargs
128 @readonly
129 def Args(self) -> Tuple:
130 """
131 A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are
132 passed without modification to :class:`~ArgumentParser`.
133 """
134 return self._args
136 @readonly
137 def KWArgs(self) -> Dict:
138 """
139 A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are
140 passed without modification to :class:`~ArgumentParser`.
141 """
142 return self._kwargs
145@export
146class CommandGroupAttribute(ArgParseAttribute):
147 """
148 *Experimental* attribute to group sub-commands in groups for better readability in a ``prog.py --help`` call.
149 """
150 __groupName: str = None
152 def __init__(self, groupName: str) -> None:
153 """
154 The constructor expects a 'groupName' which can be used to group sub-commands for better readability.
155 """
156 super().__init__()
157 self.__groupName = groupName
159 @readonly
160 def GroupName(self) -> str:
161 """Returns the name of the command group."""
162 return self.__groupName
165# @export
166# class _KwArgsMixin(metaclass=ExtendedType, mixin=True):
167# """
168# A mixin-class that offers a class field for named parameters (```**kwargs``) and a matching property.
169# """
170# _kwargs: Dict #: A dictionary of additional keyword parameters.
171#
172# @readonly
173# def KWArgs(self) -> Dict:
174# """
175# A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are
176# passed without modification to :class:`~ArgumentParser`.
177# """
178# return self._kwargs
179#
180#
181# @export
182# class _ArgsMixin(_KwArgsMixin, mixin=True):
183# """
184# A mixin-class that offers a class field for positional parameters (```*args``) and a matching property.
185# """
186#
187# _args: Tuple #: A tuple of additional positional parameters.
188#
189# @readonly
190# def Args(self) -> Tuple:
191# """
192# A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are
193# passed without modification to :class:`~ArgumentParser`.
194# """
195# return self._args
198@export
199class DefaultHandler(ArgParseAttribute, _HandlerMixin):
200 """
201 Marks a handler method as *default* handler. This method is called if no sub-command is given.
203 It's an error, if more than one method is annotated with this attribute.
204 """
206 def __call__(self, func: Callable) -> Callable:
207 self._handler = func
208 return super().__call__(func)
211@export
212class CommandHandler(ArgParseAttribute, _HandlerMixin): #, _KwArgsMixin):
213 """Marks a handler method as responsible for the given 'command'. This constructs
214 a sub-command parser using :meth:`~ArgumentParser.add_subparsers`.
215 """
217 _command: str
218 _help: str
219 # FIXME: extract to mixin?
220 _args: Tuple
221 _kwargs: Dict
223 def __init__(self, command: str, help: str = "", **kwargs: Any) -> None:
224 """The constructor expects a 'command' and an optional list of named parameters
225 (keyword arguments) which are passed without modification to :meth:`~ArgumentParser.add_subparsers`.
226 """
227 super().__init__()
228 self._command = command
229 self._help = help
230 self._args = tuple()
231 self._kwargs = kwargs
233 self._kwargs["help"] = help
235 def __call__(self, func: M) -> M:
236 self._handler = func
237 return super().__call__(func)
239 @readonly
240 def Command(self) -> str:
241 """Returns the 'command' a sub-command parser adheres to."""
242 return self._command
244# FIXME: extract to mixin?
245 @readonly
246 def Args(self) -> Tuple:
247 """
248 A tuple of additional positional parameters (``*args``) passed to the attribute. These additional parameters are
249 passed without modification to :class:`~ArgumentParser`.
250 """
251 return self._args
253 # FIXME: extract to mixin?
254 @readonly
255 def KWArgs(self) -> Dict:
256 """
257 A dictionary of additional named parameters (``**kwargs``) passed to the attribute. These additional parameters are
258 passed without modification to :class:`~ArgumentParser`.
259 """
260 return self._kwargs
263@export
264class ArgParseHelperMixin(metaclass=ExtendedType, mixin=True):
265 """
266 Mixin-class to implement an :mod:`argparse`-base command line argument processor.
267 """
268 _mainParser: ArgumentParser
269 _formatter: Any # TODO: Find type
270 _subParser: Any # TODO: Find type
271 _subParsers: Dict # TODO: Find type
273 def __init__(self, **kwargs: Any) -> None:
274 """
275 The mixin-constructor expects an optional list of named parameters which are passed without modification to the
276 :class:`ArgumentParser` constructor.
277 """
278 from .Argument import CommandLineArgument
280 super().__init__()
282 self._subParser = None
283 self._subParsers = {}
284 self._formatter = kwargs["formatter_class"] if "formatter_class" in kwargs else None
286 if "formatter_class" in kwargs: 286 ↛ 287line 286 didn't jump to line 287 because the condition on line 286 was never true
287 self._formatter = kwargs["formatter_class"]
288 if "allow_abbrev" not in kwargs: 288 ↛ 290line 288 didn't jump to line 290 because the condition on line 288 was always true
289 kwargs["allow_abbrev"] = False
290 if "exit_on_error" not in kwargs: 290 ↛ 294line 290 didn't jump to line 294 because the condition on line 290 was always true
291 kwargs["exit_on_error"] = False
293 # create a commandline argument parser
294 self._mainParser = ArgumentParser(**kwargs)
296 # Search for 'DefaultHandler' marked method
297 methods = self.GetMethodsWithAttributes(predicate=DefaultHandler)
298 if (methodCount := len(methods)) == 1: 298 ↛ 311line 298 didn't jump to line 311 because the condition on line 298 was always true
299 defaultMethod, attributes = firstPair(methods)
300 if len(attributes) > 1: 300 ↛ 301line 300 didn't jump to line 301 because the condition on line 300 was never true
301 raise ArgParseException("Marked default handler multiple times with 'DefaultAttribute'.")
303 # set default handler for the main parser
304 self._mainParser.set_defaults(func=firstElement(attributes).Handler)
306 # Add argument descriptions for the main parser
307 methodAttributes = defaultMethod.GetAttributes(CommandLineArgument) # ArgumentAttribute)
308 for methodAttribute in methodAttributes:
309 self._mainParser.add_argument(*methodAttribute.Args, **methodAttribute.KWArgs)
311 elif methodCount > 1:
312 raise ArgParseException("Marked more then one handler as default handler with 'DefaultAttribute'.")
314 # Search for 'CommandHandler' marked methods
315 methods: Dict[Callable, Tuple[CommandHandler]] = self.GetMethodsWithAttributes(predicate=CommandHandler)
316 for method, attributes in methods.items():
317 if self._subParser is None:
318 self._subParser = self._mainParser.add_subparsers(help='sub-command help')
320 if len(attributes) > 1: 320 ↛ 321line 320 didn't jump to line 321 because the condition on line 320 was never true
321 raise ArgParseException("Marked command handler multiple times with 'CommandHandler'.")
323 # Add a sub parser for each command / handler pair
324 attribute = firstElement(attributes)
325 kwArgs = attribute.KWArgs.copy()
326 if "formatter_class" not in kwArgs and self._formatter is not None: 326 ↛ 327line 326 didn't jump to line 327 because the condition on line 326 was never true
327 kwArgs["formatter_class"] = self._formatter
329 kwArgs["allow_abbrev"] = False if "allow_abbrev" not in kwargs else kwargs["allow_abbrev"]
331 subParser = self._subParser.add_parser(attribute.Command, **kwArgs)
332 subParser.set_defaults(func=attribute.Handler)
334 # Add arguments for the sub-parsers
335 methodAttributes = method.GetAttributes(CommandLineArgument) # ArgumentAttribute)
336 for methodAttribute in methodAttributes:
337 subParser.add_argument(*methodAttribute.Args, **methodAttribute.KWArgs)
339 self._subParsers[attribute.Command] = subParser
341 def Run(self, enableAutoComplete: bool = True) -> None:
342 if enableAutoComplete: 342 ↛ 345line 342 didn't jump to line 345 because the condition on line 342 was always true
343 self._EnabledAutoComplete()
345 self._ParseArguments()
347 def _EnabledAutoComplete(self) -> None:
348 try:
349 from argcomplete import autocomplete
350 autocomplete(self._mainParser)
351 except ImportError: # pragma: no cover
352 pass
354 def _ParseArguments(self) -> None:
355 # parse command line options and process split arguments in callback functions
356 parsed, args = self._mainParser.parse_known_args()
357 self._RouteToHandler(parsed)
359 def _RouteToHandler(self, args: Namespace) -> None:
360 # because func is a function (unbound to an object), it MUST be called with self as a first parameter
361 args.func(self, args)
363 @readonly
364 def MainParser(self) -> ArgumentParser:
365 """Returns the main parser."""
366 return self._mainParser
368 @readonly
369 def SubParsers(self) -> Dict:
370 """Returns the sub-parsers."""
371 return self._subParsers
374# String
375# StringList
376# Path
377# PathList
378# Delimiter
379# ValuedFlag --option=value
380# ValuedFlagList --option=foo --option=bar
381# OptionalValued --option --option=foo
382# ValuedTuple