Coverage for pyTooling/CLIAbstraction/Argument.py: 88%
197 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 23:21 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 23:21 +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 2014-2016 Technische Universität Dresden - Germany, Chair of VLSI-Design, Diagnostics and Architecture #
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#
32"""
33This module implements command line arguments without prefix character(s).
35"""
36from abc import abstractmethod
37from pathlib import Path
38from sys import platform as sys_platform
39from typing import ClassVar, Union, Iterable, TypeVar, Generic, Any, Optional as Nullable
41from pyTooling.Decorators import export, readonly
42from pyTooling.MetaClasses import ExtendedType, abstractclass
43from pyTooling.Common import getFullyQualifiedName
45if sys_platform == "win32":
46 from subprocess import list2cmdline as formatCommandLine
47else:
48 from shlex import join as formatCommandLine
51__all__ = ["ValueT"]
54ValueT = TypeVar("ValueT") #: The type of value in a valued argument.
57@export
58@abstractclass
59class CommandLineArgument(metaclass=ExtendedType):
60 """
61 Base-class for all *Argument* classes.
63 An argument instance can be converted via ``AsArgument`` to a single string value or a sequence of string values
64 (tuple) usable e.g. with :class:`subprocess.Popen`. Each argument class implements at least one ``pattern`` parameter
65 to specify how argument are formatted.
67 There are multiple derived formats supporting:
69 * commands |br|
70 |rarr| :mod:`~pyTooling.CLIAbstraction.Command`
71 * simple names (flags) |br|
72 |rarr| :mod:`~pyTooling.CLIAbstraction.Flag`, :mod:`~pyTooling.CLIAbstraction.BooleanFlag`
73 * simple values (vlaued flags) |br|
74 |rarr| :class:`~pyTooling.CLIAbstraction.Argument.StringArgument`, :class:`~pyTooling.CLIAbstraction.Argument.PathArgument`
75 * names and values |br|
76 |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag`, :mod:`~pyTooling.CLIAbstraction.OptionalValuedFlag`
77 * key-value pairs |br|
78 |rarr| :class:`~pyTooling.CLIAbstraction.KeyValueFlag.NamedKeyValuePairsArgument`
79 """
81 _PATTERN: ClassVar[str] #: Format string to render the argument on the command line.
83 def __init_subclass__(cls, *args: Any, pattern: Nullable[str] = None, **kwargs: Any) -> None:
84 """
85 This method is called when a class is derived.
87 :param args: Any positional arguments.
88 :param pattern: Optional, this pattern is used to format an argument. |br|
89 Default: ``None``.
90 :param kwargs: Any keyword argument.
91 """
92 super().__init_subclass__(*args, **kwargs)
93 cls._PATTERN = pattern
95 # TODO: Add property to read pattern
97 @abstractmethod
98 def AsArgument(self) -> Union[str, Iterable[str]]: # type: ignore[empty-body]
99 """
100 Convert this argument instance to a string representation using the matching pattern based on
101 the internal name and value.
103 The result isn't escaped: :class:`subprocess.Popen` passes each string to the program as one argument.
104 :meth:`__str__` returns it escaped for a command line.
106 :returns: Formatted argument.
107 :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass.
108 """
109 raise NotImplementedError("Method 'AsArgument' is an abstract method and must be implemented by a subclass.")
111 def __str__(self) -> str:
112 """
113 Return this argument as written on a command line, escaped for the current platform.
115 :returns: The formatted argument, escaped by ``formatCommandLine``: :func:`shlex.join`, on Windows
116 ``subprocess.list2cmdline()``.
117 """
118 argument = self.AsArgument()
119 if isinstance(argument, str):
120 return formatCommandLine((argument, ))
121 else:
122 return formatCommandLine(argument)
124 @staticmethod
125 def _DoubleQuotedLiteral(value: str) -> str:
126 """
127 Return a string as a Python literal in double quotes, escaped as :func:`repr` escapes it.
129 :param value: The string.
130 :returns: The string's literal.
131 """
132 literal = repr(value)
133 if literal[0] == "\"":
134 return literal
136 return "\"" + literal[1:-1].replace("\\'", "'").replace("\"", "\\\"") + "\""
138 def __repr__(self) -> str:
139 """
140 Return this argument as a Python literal, with strings in double quotes.
142 :returns: The formatted argument (:meth:`AsArgument`) as a literal; a sequence as a list.
143 """
144 argument = self.AsArgument()
145 if isinstance(argument, str):
146 return self._DoubleQuotedLiteral(argument)
147 else:
148 literal = self._DoubleQuotedLiteral
149 return "[" + ", ".join([literal(item) for item in argument]) + "]" # WORKAROUND: Python <3.12
150 # return f"[{", ".join([literal(item) for item in argument])}]"
153@export
154class ExecutableArgument(CommandLineArgument):
155 """
156 Represents the executable.
157 """
159 _executable: Path #: Path to the executable this argument represents.
161 def __init__(self, executable: Path) -> None:
162 """
163 Initializes a ExecutableArgument instance.
165 :param executable: Path to the executable.
166 :raises TypeError: If parameter 'executable' is not of type :class:`~pathlib.Path`.
167 """
168 if not isinstance(executable, Path):
169 ex = TypeError("Parameter 'executable' is not of type 'Path'.")
170 ex.add_note(f"Got type '{getFullyQualifiedName(executable)}'.")
171 raise ex
173 self._executable = executable
175 @property
176 def Executable(self) -> Path:
177 """
178 Property to access the path to the wrapped executable (:attr:`_executable`).
180 :returns: Internal path to the executable.
181 :raises TypeError: If an assigned value is not of type :class:`~pathlib.Path`.
182 """
183 return self._executable
185 @Executable.setter
186 def Executable(self, value: Path) -> None:
187 if not isinstance(value, Path):
188 ex = TypeError("Parameter 'value' is not of type 'Path'.")
189 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
190 raise ex
192 self._executable = value
194 def AsArgument(self) -> Union[str, Iterable[str]]:
195 """
196 Convert this argument instance to a string representation using the matching pattern based on
197 the internal path to the wrapped executable.
199 :returns: Formatted argument.
200 """
201 return f"{self._executable}"
204@export
205class DelimiterArgument(CommandLineArgument, pattern="--"):
206 """
207 Represents a delimiter symbol like ``--``.
208 """
210 def __init_subclass__(cls, *args: Any, pattern: str = "--", **kwargs: Any) -> None:
211 """
212 This method is called when a class is derived.
214 :param args: Any positional arguments.
215 :param pattern: Optional, this pattern is used to format an argument. |br|
216 Default: ``"--"``.
217 :param kwargs: Any keyword argument.
218 """
219 kwargs["pattern"] = pattern
220 super().__init_subclass__(*args, **kwargs)
222 def AsArgument(self) -> Union[str, Iterable[str]]:
223 """
224 Convert this argument instance to a string representation using the matching pattern.
226 :returns: Formatted argument.
227 """
228 return self._PATTERN
231@export
232@abstractclass
233class NamedArgument(CommandLineArgument, pattern="{0}"):
234 """
235 Base-class for all command line arguments with a name.
236 """
238 _NAME: ClassVar[str] #: Name of the argument, inserted into :attr:`_PATTERN`.
240 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", **kwargs: Any) -> None:
241 """
242 This method is called when a class is derived.
244 :param args: Any positional arguments.
245 :param name: Optional, name of the CLI argument.
246 :param pattern: Optional, this pattern is used to format an argument. |br|
247 Default: ``"{0}"``.
248 :param kwargs: Any keyword argument.
249 """
250 kwargs["pattern"] = pattern
251 super().__init_subclass__(*args, **kwargs)
252 cls._NAME = name
254 @readonly
255 def Name(self) -> str:
256 """
257 Get the internal name.
259 :returns: Internal name.
260 """
261 return self._NAME
263 def AsArgument(self) -> Union[str, Iterable[str]]:
264 """
265 Convert this argument instance to a string representation using the matching pattern based on
266 the internal name.
268 :returns: Formatted argument.
269 :raises ValueError: If internal name is None.
270 """
271 if self._NAME is None: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true
272 raise ValueError("Internal value '_NAME' is None.")
274 return self._PATTERN.format(self._NAME)
277@export
278class ValuedArgument(CommandLineArgument, Generic[ValueT], pattern="{0}"):
279 """
280 Base-class for all command line arguments with a value.
281 """
283 _value: ValueT #: Value of the argument, inserted into :attr:`_PATTERN`.
285 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any) -> None:
286 """
287 This method is called when a class is derived.
289 :param args: Any positional arguments.
290 :param pattern: Optional, this pattern is used to format an argument. |br|
291 Default: ``"{0}"``.
292 :param kwargs: Any keyword argument.
293 """
294 kwargs["pattern"] = pattern
295 super().__init_subclass__(*args, **kwargs)
297 def __init__(self, value: ValueT) -> None:
298 """
299 Initializes a ValuedArgument instance.
301 :param value: Value to be stored internally.
302 :raises ValueError: If parameter 'value' is None.
303 :raises ValueError: If parameter 'value' is None.
304 """
305 if value is None: 305 ↛ 306line 305 didn't jump to line 306 because the condition on line 305 was never true
306 raise ValueError("Parameter 'value' is None.")
308 self._value = value
310 @property
311 def Value(self) -> ValueT:
312 """
313 Property to access the internal value (:attr:`_value`).
315 :returns: Internal value.
316 :raises ValueError: If ``None`` is assigned.
317 """
318 return self._value
320 @Value.setter
321 def Value(self, value: ValueT) -> None:
322 if value is None: 322 ↛ 323line 322 didn't jump to line 323 because the condition on line 322 was never true
323 raise ValueError("Value to set is None.")
325 self._value = value
327 def AsArgument(self) -> Union[str, Iterable[str]]:
328 """
329 Convert this argument instance to a string representation using the matching pattern based on
330 the internal value.
332 :returns: Formatted argument.
333 """
334 return self._PATTERN.format(self._value)
337class NamedAndValuedArgument(NamedArgument, ValuedArgument[ValueT], Generic[ValueT], pattern="{0}={1}"):
338 """
339 Base-class for all command line arguments with a name and a value.
340 """
342 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}={1}", **kwargs: Any) -> None:
343 """
344 This method is called when a class is derived.
346 :param args: Any positional arguments.
347 :param name: Optional, name of the CLI argument.
348 :param pattern: Optional, this pattern is used to format an argument. |br|
349 Default: ``"{0}={1}"``.
350 :param kwargs: Any keyword argument.
351 """
352 kwargs["name"] = name
353 kwargs["pattern"] = pattern
354 super().__init_subclass__(*args, **kwargs)
355 del kwargs["name"]
356 del kwargs["pattern"]
357 ValuedArgument.__init_subclass__(*args, **kwargs)
359 def __init__(self, value: ValueT) -> None:
360 """
361 Initialize the argument with the value rendered into its pattern.
363 :param value: Value of the argument.
364 """
365 ValuedArgument.__init__(self, value)
367 def AsArgument(self) -> Union[str, Iterable[str]]:
368 """
369 Convert this argument instance to a string representation using the matching pattern based on
370 the internal name and value.
372 :returns: Formatted argument.
373 :raises ValueError: If internal name is None.
374 """
375 if self._NAME is None: 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true
376 raise ValueError("Internal value '_NAME' is None.")
378 return self._PATTERN.format(self._NAME, self._value)
381@abstractclass
382class NamedTupledArgument(NamedArgument, ValuedArgument[ValueT], Generic[ValueT], pattern="{0}"):
383 """
384 Class and base-class for all TupleFlag classes, which represents an argument with separate value.
386 A tuple argument is a command line argument followed by a separate value. Name and value are passed as two arguments
387 to the executable.
389 **Example: **
391 * `width 100``
392 """
394 _VALUE_PATTERN: ClassVar[str] #: Format string to render the argument's value as a second command line element.
396 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", valuePattern: str = "{0}", **kwargs: Any) -> None:
397 """
398 This method is called when a class is derived.
400 :param args: Any positional arguments.
401 :param name: Optional, name of the CLI argument.
402 :param pattern: Optional, this pattern is used to format the CLI argument name. |br|
403 Default: ``"{0}"``.
404 :param valuePattern: Optional, this pattern is used to format the value. |br|
405 Default: ``"{0}"``.
406 :param kwargs: Any keyword argument.
407 """
408 kwargs["name"] = name
409 kwargs["pattern"] = pattern
410 super().__init_subclass__(*args, **kwargs)
411 cls._VALUE_PATTERN = valuePattern
413 def __init__(self, value: ValueT) -> None:
414 """
415 Initialize the argument with the value rendered into its pattern.
417 :param value: Value of the argument.
418 """
419 ValuedArgument.__init__(self, value)
421 # TODO: Add property to read value pattern
423 # @property
424 # def ValuePattern(self) -> str:
425 # if self._VALUE_PATTERN is None:
426 # raise ValueError("Internal value '_VALUE_PATTERN' is None.")
427 #
428 # return self._VALUE_PATTERN
430 def AsArgument(self) -> Union[str, Iterable[str]]:
431 """
432 Convert this argument instance to a sequence of string representations using the matching
433 pattern based on the internal name and value.
435 :returns: Formatted argument as tuple of strings.
436 :raises ValueError: If internal name is None.
437 """
438 if self._NAME is None: 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true
439 raise ValueError("Internal value '_NAME' is None.")
441 return (
442 self._PATTERN.format(self._NAME),
443 self._VALUE_PATTERN.format(self._value)
444 )
447@export
448class StringArgument(ValuedArgument[str], pattern="{0}"):
449 """
450 Represents a simple string argument.
452 A list of strings is available as :class:`~pyTooling.CLIAbstraction.Argument.StringListArgument`.
453 """
455 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any) -> None:
456 """
457 This method is called when a class is derived.
459 :param args: Any positional arguments.
460 :param pattern: Optional, this pattern is used to format an argument. |br|
461 Default: ``"{0}"``.
462 :param kwargs: Any keyword argument.
463 """
464 kwargs["pattern"] = pattern
465 super().__init_subclass__(*args, **kwargs)
468@export
469class StringListArgument(ValuedArgument[str]):
470 """
471 Represents a list of string argument (:class:`~pyTooling.CLIAbstraction.Argument.StringArgument`)."""
473 def __init__(self, values: Iterable[str]) -> None:
474 """
475 Initializes a StringListArgument instance.
477 :param values: An iterable of str instances.
478 :raises TypeError: If iterable parameter 'values' contains elements not of type string.
479 """
480 self._values = []
481 for value in values:
482 if not isinstance(value, str): 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true
483 ex = TypeError("Parameter 'values' contains elements which are not of type 'str'.")
484 ex.add_note(f"Got type '{getFullyQualifiedName(values)}'.")
485 raise ex
487 self._values.append(value)
489 @property
490 def Value(self) -> list[str]:
491 """
492 Property to access the internal list of str objects (:attr:`_values`).
494 .. note:: On assignment, the list object is not replaced, but cleared and then reused by adding the given elements
495 of the iterable.
497 :returns: Reference to the internal list of str objects.
498 :raises TypeError: If an assigned iterable contains elements which are not of type string.
499 """
500 return self._values
502 @Value.setter
503 def Value(self, value: Iterable[str]) -> None:
504 self._values.clear()
505 for value in value:
506 if not isinstance(value, str):
507 ex = TypeError("Value contains elements which are not of type 'str'.")
508 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
509 raise ex
510 self._values.append(value)
512 def AsArgument(self) -> Union[str, Iterable[str]]:
513 """
514 Convert this argument instance to a string representation using the matching pattern based on
515 the internal value.
517 :returns: Sequence of formatted arguments.
518 """
519 return [f"{value}" for value in self._values]
522# TODO: Add option to class if path should be checked for existence
523@export
524class PathArgument(CommandLineArgument):
525 """
526 Represents a single path argument.
528 A list of paths is available as :class:`~pyTooling.CLIAbstraction.Argument.PathListArgument`.
529 """
530 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`.
531 _path: Path #: Path this argument represents.
533 def __init__(self, path: Path) -> None:
534 """
535 Initializes a PathArgument instance.
537 :param path: Path to a filesystem object.
538 :raises TypeError: If parameter 'path' is not of type :class:`~pathlib.Path`.
539 """
540 if not isinstance(path, Path): 540 ↛ 541line 540 didn't jump to line 541 because the condition on line 540 was never true
541 ex = TypeError("Parameter 'path' is not of type 'Path'.")
542 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
543 raise ex
544 self._path = path
546 @property
547 def Value(self) -> Path:
548 """
549 Property to access the internal path object (:attr:`_path`).
551 :returns: Internal path object.
552 :raises TypeError: If an assigned value is not of type :class:`~pathlib.Path`.
553 """
554 return self._path
556 @Value.setter
557 def Value(self, value: Path) -> None:
558 if not isinstance(value, Path): 558 ↛ 559line 558 didn't jump to line 559 because the condition on line 558 was never true
559 ex = TypeError("Value is not of type 'Path'.")
560 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
561 raise ex
563 self._path = value
565 def AsArgument(self) -> Union[str, Iterable[str]]:
566 """
567 Convert this argument instance to a string representation using the matching pattern based on
568 the internal value.
570 :returns: Formatted argument.
571 """
572 return f"{self._path}"
575@export
576class PathListArgument(CommandLineArgument):
577 """
578 Represents a list of path arguments (:class:`~pyTooling.CLIAbstraction.Argument.PathArgument`).
579 """
580 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`.
581 _paths: list[Path] #: Paths this argument represents.
583 def __init__(self, paths: Iterable[Path]) -> None:
584 """
585 Initializes a PathListArgument instance.
587 :param paths: An iterable os Path instances.
588 :raises TypeError: If iterable parameter 'paths' contains elements not of type :class:`~pathlib.Path`.
589 """
590 self._paths = []
591 for path in paths:
592 if not isinstance(path, Path): 592 ↛ 593line 592 didn't jump to line 593 because the condition on line 592 was never true
593 ex = TypeError("Parameter 'paths' contains elements which are not of type 'Path'.")
594 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
595 raise ex
597 self._paths.append(path)
599 @property
600 def Value(self) -> list[Path]:
601 """
602 Property to access the internal list of path objects (:attr:`_paths`).
604 .. note:: On assignment, the list object is not replaced, but cleared and then reused by adding the given elements
605 of the iterable.
607 :returns: Reference to the internal list of path objects.
608 :raises TypeError: If an assigned iterable contains elements which are not of type :class:`~pathlib.Path`.
609 """
610 return self._paths
612 @Value.setter
613 def Value(self, value: Iterable[Path]) -> None:
614 self._paths.clear()
615 for path in value:
616 if not isinstance(path, Path):
617 ex = TypeError("Value contains elements which are not of type 'Path'.")
618 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
619 raise ex
620 self._paths.append(path)
622 def AsArgument(self) -> Union[str, Iterable[str]]:
623 """
624 Convert this argument instance to a string representation using the matching pattern based on
625 the internal value.
627 :returns: Sequence of formatted arguments.
628 """
629 return [f"{path}" for path in self._paths]