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

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 

34 

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 

40 

41 

42M = TypeVar("M", bound=Callable) 

43 

44 

45@export 

46class ArgParseException(ToolingException): 

47 pass 

48 

49 

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 """ 

56 

57 

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. 

64 

65 @readonly 

66 def Handler(self) -> Callable: 

67 """Returns the handler method.""" 

68 return self._handler 

69 

70 

71# FIXME: Is _HandlerMixin needed here, or for commands? 

72@export 

73class CommandLineArgument(ArgParseAttribute, _HandlerMixin): 

74 """ 

75 Base-class for all *Argument* classes. 

76 

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. 

80 

81 There are multiple derived formats supporting: 

82 

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 """ 

94 

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) 

102 

103 _args: Tuple 

104 _kwargs: Dict 

105 

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 

114 

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 

122 

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 

130 

131 

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 

138 

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 

145 

146 @readonly 

147 def GroupName(self) -> str: 

148 """Returns the name of the command group.""" 

149 return self.__groupName 

150 

151 

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 

183 

184 

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. 

189 

190 It's an error, if more than one method is annotated with this attribute. 

191 """ 

192 

193 def __call__(self, func: Callable) -> Callable: 

194 self._handler = func 

195 return super().__call__(func) 

196 

197 

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 """ 

203 

204 _command: str 

205 _help: str 

206 # FIXME: extract to mixin? 

207 _args: Tuple 

208 _kwargs: Dict 

209 

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 

219 

220 self._kwargs["help"] = help 

221 

222 def __call__(self, func: M) -> M: 

223 self._handler = func 

224 return super().__call__(func) 

225 

226 @readonly 

227 def Command(self) -> str: 

228 """Returns the 'command' a sub-command parser adheres to.""" 

229 return self._command 

230 

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 

239 

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 

248 

249 

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 

259 

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 

266 

267 super().__init__() 

268 

269 self._subParser = None 

270 self._subParsers = {} 

271 self._formatter = kwargs["formatter_class"] if "formatter_class" in kwargs else None 

272 

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 

279 

280 # create a commandline argument parser 

281 self._mainParser = ArgumentParser(**kwargs) 

282 

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'.") 

289 

290 # set default handler for the main parser 

291 self._mainParser.set_defaults(func=firstElement(attributes).Handler) 

292 

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) 

297 

298 elif methodCount > 1: 

299 raise ArgParseException("Marked more then one handler as default handler with 'DefaultAttribute'.") 

300 

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') 

306 

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'.") 

309 

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 

315 

316 kwArgs["allow_abbrev"] = False if "allow_abbrev" not in kwargs else kwargs["allow_abbrev"] 

317 

318 subParser = self._subParser.add_parser(attribute.Command, **kwArgs) 

319 subParser.set_defaults(func=attribute.Handler) 

320 

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) 

325 

326 self._subParsers[attribute.Command] = subParser 

327 

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() 

331 

332 self._ParseArguments() 

333 

334 def _EnabledAutoComplete(self) -> None: 

335 try: 

336 from argcomplete import autocomplete 

337 autocomplete(self._mainParser) 

338 except ImportError: # pragma: no cover 

339 pass 

340 

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) 

345 

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) 

349 

350 @readonly 

351 def MainParser(self) -> ArgumentParser: 

352 """Returns the main parser.""" 

353 return self._mainParser 

354 

355 @readonly 

356 def SubParsers(self) -> Dict: 

357 """Returns the sub-parsers.""" 

358 return self._subParsers 

359 

360 

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 

370