Coverage for pyTooling / CLIAbstraction / Argument.py: 89%
226 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 22:22 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 22:22 +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 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).
36"""
37from abc import abstractmethod
38from pathlib import Path
39from typing import ClassVar, List, Union, Iterable, TypeVar, Generic, Any, Optional as Nullable
41try:
42 from pyTooling.Decorators import export, readonly
43 from pyTooling.Common import getFullyQualifiedName
44except (ImportError, ModuleNotFoundError): # pragma: no cover
45 print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!")
47 try:
48 from Decorators import export, readonly
49 from Common import getFullyQualifiedName
50 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
51 print("[pyTooling.Versioning] Could not import directly!")
52 raise ex
55__all__ = ["ValueT"]
58ValueT = TypeVar("ValueT") #: The type of value in a valued argument.
61@export
62class CommandLineArgument:
63 """
64 Base-class for all *Argument* classes.
66 An argument instance can be converted via ``AsArgument`` to a single string value or a sequence of string values
67 (tuple) usable e.g. with :class:`subprocess.Popen`. Each argument class implements at least one ``pattern`` parameter
68 to specify how argument are formatted.
70 There are multiple derived formats supporting:
72 * commands |br|
73 |rarr| :mod:`~pyTooling.CLIAbstraction.Command`
74 * simple names (flags) |br|
75 |rarr| :mod:`~pyTooling.CLIAbstraction.Flag`, :mod:`~pyTooling.CLIAbstraction.BooleanFlag`
76 * simple values (vlaued flags) |br|
77 |rarr| :class:`~pyTooling.CLIAbstraction.Argument.StringArgument`, :class:`~pyTooling.CLIAbstraction.Argument.PathArgument`
78 * names and values |br|
79 |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag`, :mod:`~pyTooling.CLIAbstraction.OptionalValuedFlag`
80 * key-value pairs |br|
81 |rarr| :mod:`~pyTooling.CLIAbstraction.NamedKeyValuePair`
82 """
84 _pattern: ClassVar[str]
86 def __init_subclass__(cls, *args: Any, pattern: Nullable[str] = None, **kwargs: Any):
87 """This method is called when a class is derived.
89 :param args: Any positional arguments.
90 :param pattern: This pattern is used to format an argument.
91 :param kwargs: Any keyword argument.
92 """
93 super().__init_subclass__(*args, **kwargs)
94 cls._pattern = pattern
96 def __new__(cls, *args: Any, **kwargs: Any):
97 if cls is CommandLineArgument:
98 raise TypeError(f"Class '{cls.__name__}' is abstract.")
100 # TODO: not sure why parameters meant for __init__ do reach this level and distract __new__ from it's work
101 return super().__new__(cls)
103 # TODO: Add property to read pattern
105 @abstractmethod
106 def AsArgument(self) -> Union[str, Iterable[str]]: # type: ignore[empty-body]
107 """
108 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
109 the internal name and value.
111 :return: Formatted argument.
112 :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass.
113 """
114 raise NotImplementedError(f"Method 'AsArgument' is an abstract method and must be implemented by a subclass.")
116 @abstractmethod
117 def __str__(self) -> str: # type: ignore[empty-body]
118 """
119 Return a string representation of this argument instance.
121 :return: Argument formatted and enclosed in double quotes.
122 :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass.
123 """
124 raise NotImplementedError(f"Method '__str__' is an abstract method and must be implemented by a subclass.")
126 @abstractmethod
127 def __repr__(self) -> str: # type: ignore[empty-body]
128 """
129 Return a string representation of this argument instance.
131 .. note:: By default, this method is identical to :meth:`__str__`.
133 :return: Argument formatted and enclosed in double quotes.
134 :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass.
135 """
136 raise NotImplementedError(f"Method '__repr__' is an abstract method and must be implemented by a subclass.")
139@export
140class ExecutableArgument(CommandLineArgument):
141 """
142 Represents the executable.
143 """
145 _executable: Path
147 def __init__(self, executable: Path) -> None:
148 """
149 Initializes a ExecutableArgument instance.
151 :param executable: Path to the executable.
152 :raises TypeError: If parameter 'executable' is not of type :class:`~pathlib.Path`.
153 """
154 if not isinstance(executable, Path):
155 ex = TypeError("Parameter 'executable' is not of type 'Path'.")
156 ex.add_note(f"Got type '{getFullyQualifiedName(executable)}'.")
157 raise ex
159 self._executable = executable
161 @property
162 def Executable(self) -> Path:
163 """
164 Get the internal path to the wrapped executable.
166 :return: Internal path to the executable.
167 """
168 return self._executable
170 @Executable.setter
171 def Executable(self, value):
172 """
173 Set the internal path to the wrapped executable.
175 :param value: Value to path to the executable.
176 :raises TypeError: If value is not of type :class:`~pathlib.Path`.
177 """
178 if not isinstance(value, Path):
179 ex = TypeError("Parameter 'value' is not of type 'Path'.")
180 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
181 raise ex
183 self._executable = value
185 def AsArgument(self) -> Union[str, Iterable[str]]:
186 """
187 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
188 the internal path to the wrapped executable.
190 :return: Formatted argument.
191 """
192 return f"{self._executable}"
194 def __str__(self) -> str:
195 """
196 Return a string representation of this argument instance.
198 :return: Argument formatted and enclosed in double quotes.
199 """
200 return f"\"{self._executable}\""
202 __repr__ = __str__
205@export
206class DelimiterArgument(CommandLineArgument, pattern="--"):
207 """
208 Represents a delimiter symbol like ``--``.
209 """
211 def __init_subclass__(cls, *args: Any, pattern: str = "--", **kwargs: Any):
212 """
213 This method is called when a class is derived.
215 :param args: Any positional arguments.
216 :param pattern: This pattern is used to format an argument. 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 with proper escaping using the matching pattern.
226 :return: Formatted argument.
227 """
228 return self._pattern
230 def __str__(self) -> str:
231 """
232 Return a string representation of this argument instance.
234 :return: Argument formatted and enclosed in double quotes.
235 """
236 return f"\"{self._pattern}\""
238 __repr__ = __str__
241@export
242class NamedArgument(CommandLineArgument, pattern="{0}"):
243 """
244 Base-class for all command line arguments with a name.
245 """
247 _name: ClassVar[str]
249 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", **kwargs: Any):
250 """
251 This method is called when a class is derived.
253 :param args: Any positional arguments.
254 :param name: Name of the argument.
255 :param pattern: This pattern is used to format an argument.
256 :param kwargs: Any keyword argument.
257 """
258 kwargs["pattern"] = pattern
259 super().__init_subclass__(*args, **kwargs)
260 cls._name = name
262 def __new__(cls, *args: Any, **kwargs: Any):
263 if cls is NamedArgument:
264 raise TypeError(f"Class '{cls.__name__}' is abstract.")
265 return super().__new__(cls, *args, **kwargs)
267 @readonly
268 def Name(self) -> str:
269 """
270 Get the internal name.
272 :return: Internal name.
273 """
274 return self._name
276 def AsArgument(self) -> Union[str, Iterable[str]]:
277 """
278 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
279 the internal name.
281 :return: Formatted argument.
282 :raises ValueError: If internal name is None.
283 """
284 if self._name is None: 284 ↛ 285line 284 didn't jump to line 285 because the condition on line 284 was never true
285 raise ValueError(f"Internal value '_name' is None.")
287 return self._pattern.format(self._name)
289 def __str__(self) -> str:
290 """
291 Return a string representation of this argument instance.
293 :return: Argument formatted and enclosed in double quotes.
294 """
295 return f"\"{self.AsArgument()}\""
297 __repr__ = __str__
300@export
301class ValuedArgument(CommandLineArgument, Generic[ValueT], pattern="{0}"):
302 """
303 Base-class for all command line arguments with a value.
304 """
306 _value: ValueT
308 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any):
309 """
310 This method is called when a class is derived.
312 :param args: Any positional arguments.
313 :param pattern: This pattern is used to format an argument.
314 :param kwargs: Any keyword argument.
315 """
316 kwargs["pattern"] = pattern
317 super().__init_subclass__(*args, **kwargs)
319 def __init__(self, value: ValueT) -> None:
320 """
321 Initializes a ValuedArgument instance.
323 :param value: Value to be stored internally.
324 :raises TypeError: If parameter 'value' is None.
325 """
326 if value is None: 326 ↛ 327line 326 didn't jump to line 327 because the condition on line 326 was never true
327 raise ValueError("Parameter 'value' is None.")
329 self._value = value
331 @property
332 def Value(self) -> ValueT:
333 """
334 Get the internal value.
336 :return: Internal value.
337 """
338 return self._value
340 @Value.setter
341 def Value(self, value: ValueT) -> None:
342 """
343 Set the internal value.
345 :param value: Value to set.
346 :raises ValueError: If value to set is None.
347 """
348 if value is None: 348 ↛ 349line 348 didn't jump to line 349 because the condition on line 348 was never true
349 raise ValueError(f"Value to set is None.")
351 self._value = value
353 def AsArgument(self) -> Union[str, Iterable[str]]:
354 """
355 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
356 the internal value.
358 :return: Formatted argument.
359 """
360 return self._pattern.format(self._value)
362 def __str__(self) -> str:
363 """
364 Return a string representation of this argument instance.
366 :return: Argument formatted and enclosed in double quotes.
367 """
368 return f"\"{self.AsArgument()}\""
370 __repr__ = __str__
373class NamedAndValuedArgument(NamedArgument, ValuedArgument, Generic[ValueT], pattern="{0}={1}"):
374 """
375 Base-class for all command line arguments with a name and a value.
376 """
378 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}={1}", **kwargs: Any):
379 """
380 This method is called when a class is derived.
382 :param args: Any positional arguments.
383 :param name: Name of the argument.
384 :param pattern: This pattern is used to format an argument.
385 :param kwargs: Any keyword argument.
386 """
387 kwargs["name"] = name
388 kwargs["pattern"] = pattern
389 super().__init_subclass__(*args, **kwargs)
390 del kwargs["name"]
391 del kwargs["pattern"]
392 ValuedArgument.__init_subclass__(*args, **kwargs)
394 def __init__(self, value: ValueT) -> None:
395 ValuedArgument.__init__(self, value)
397 def AsArgument(self) -> Union[str, Iterable[str]]:
398 """
399 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
400 the internal name and value.
402 :return: Formatted argument.
403 :raises ValueError: If internal name is None.
404 """
405 if self._name is None: 405 ↛ 406line 405 didn't jump to line 406 because the condition on line 405 was never true
406 raise ValueError(f"Internal value '_name' is None.")
408 return self._pattern.format(self._name, self._value)
410 def __str__(self) -> str:
411 """
412 Return a string representation of this argument instance.
414 :return: Argument formatted and enclosed in double quotes.
415 """
416 return f"\"{self.AsArgument()}\""
418 __repr__ = __str__
421class NamedTupledArgument(NamedArgument, ValuedArgument, Generic[ValueT], pattern="{0}"):
422 """
423 Class and base-class for all TupleFlag classes, which represents an argument with separate value.
425 A tuple argument is a command line argument followed by a separate value. Name and value are passed as two arguments
426 to the executable.
428 **Example: **
430 * `width 100``
431 """
433 _valuePattern: ClassVar[str]
435 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", valuePattern: str = "{0}", **kwargs: Any):
436 kwargs["name"] = name
437 kwargs["pattern"] = pattern
438 super().__init_subclass__(*args, **kwargs)
439 cls._valuePattern = valuePattern
441 def __new__(cls, *args: Any, **kwargs: Any):
442 if cls is NamedTupledArgument:
443 raise TypeError(f"Class '{cls.__name__}' is abstract.")
444 return super().__new__(cls, *args, **kwargs)
446 def __init__(self, value: ValueT) -> None:
447 ValuedArgument.__init__(self, value)
449 # TODO: Add property to read value pattern
451 # @property
452 # def ValuePattern(self) -> str:
453 # if self._valuePattern is None:
454 # raise ValueError(f"") # XXX: add message
455 #
456 # return self._valuePattern
458 def AsArgument(self) -> Union[str, Iterable[str]]:
459 """
460 Convert this argument instance to a sequence of string representations with proper escaping using the matching
461 pattern based on the internal name and value.
463 :return: Formatted argument as tuple of strings.
464 :raises ValueError: If internal name is None.
465 """
466 if self._name is None: 466 ↛ 467line 466 didn't jump to line 467 because the condition on line 466 was never true
467 raise ValueError(f"Internal value '_name' is None.")
469 return (
470 self._pattern.format(self._name),
471 self._valuePattern.format(self._value)
472 )
474 def __str__(self) -> str:
475 """
476 Return a string representation of this argument instance.
478 :return: Space separated sequence of arguments formatted and each enclosed in double quotes.
479 """
480 return " ".join([f"\"{item}\"" for item in self.AsArgument()])
482 def __repr__(self) -> str:
483 """
484 Return a string representation of this argument instance.
486 :return: Comma separated sequence of arguments formatted and each enclosed in double quotes.
487 """
488 return ", ".join([f"\"{item}\"" for item in self.AsArgument()])
491@export
492class StringArgument(ValuedArgument, pattern="{0}"):
493 """
494 Represents a simple string argument.
496 A list of strings is available as :class:`~pyTooling.CLIAbstraction.Argument.StringListArgument`.
497 """
499 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any):
500 """
501 This method is called when a class is derived.
503 :param args: Any positional arguments.
504 :param pattern: This pattern is used to format an argument.
505 :param kwargs: Any keyword argument.
506 """
507 kwargs["pattern"] = pattern
508 super().__init_subclass__(*args, **kwargs)
511@export
512class StringListArgument(ValuedArgument):
513 """
514 Represents a list of string argument (:class:`~pyTooling.CLIAbstraction.Argument.StringArgument`)."""
516 def __init__(self, values: Iterable[str]) -> None:
517 """
518 Initializes a StringListArgument instance.
520 :param values: An iterable of str instances.
521 :raises TypeError: If iterable parameter 'values' contains elements not of type :class:`str`.
522 """
523 self._values = []
524 for value in values:
525 if not isinstance(value, str): 525 ↛ 526line 525 didn't jump to line 526 because the condition on line 525 was never true
526 ex = TypeError(f"Parameter 'values' contains elements which are not of type 'str'.")
527 ex.add_note(f"Got type '{getFullyQualifiedName(values)}'.")
528 raise ex
530 self._values.append(value)
532 @property
533 def Value(self) -> List[str]:
534 """
535 Get the internal list of str objects.
537 :return: Reference to the internal list of str objects.
538 """
539 return self._values
541 @Value.setter
542 def Value(self, value: Iterable[str]):
543 """
544 Overwrite all elements in the internal list of str objects.
546 .. note:: The list object is not replaced, but cleared and then reused by adding the given elements in the iterable.
548 :param value: List of str objects to set.
549 :raises TypeError: If value contains elements, which are not of type :class:`str`.
550 """
551 self._values.clear()
552 for value in value:
553 if not isinstance(value, str):
554 ex = TypeError(f"Value contains elements which are not of type 'str'.")
555 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
556 raise ex
557 self._values.append(value)
559 def AsArgument(self) -> Union[str, Iterable[str]]:
560 """
561 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
562 the internal value.
564 :return: Sequence of formatted arguments.
565 """
566 return [f"{value}" for value in self._values]
568 def __str__(self) -> str:
569 """
570 Return a string representation of this argument instance.
572 :return: Space separated sequence of arguments formatted and each enclosed in double quotes.
573 """
574 return " ".join([f"\"{value}\"" for value in self.AsArgument()])
576 def __repr__(self) -> str:
577 """
578 Return a string representation of this argument instance.
580 :return: Comma separated sequence of arguments formatted and each enclosed in double quotes.
581 """
582 return ", ".join([f"\"{value}\"" for value in self.AsArgument()])
585# TODO: Add option to class if path should be checked for existence
586@export
587class PathArgument(CommandLineArgument):
588 """
589 Represents a single path argument.
591 A list of paths is available as :class:`~pyTooling.CLIAbstraction.Argument.PathListArgument`.
592 """
593 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`.
594 _path: Path
596 def __init__(self, path: Path) -> None:
597 """
598 Initializes a PathArgument instance.
600 :param path: Path to a filesystem object.
601 :raises TypeError: If parameter 'path' is not of type :class:`~pathlib.Path`.
602 """
603 if not isinstance(path, Path): 603 ↛ 604line 603 didn't jump to line 604 because the condition on line 603 was never true
604 ex = TypeError("Parameter 'path' is not of type 'Path'.")
605 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
606 raise ex
607 self._path = path
609 @property
610 def Value(self) -> Path:
611 """
612 Get the internal path object.
614 :return: Internal path object.
615 """
616 return self._path
618 @Value.setter
619 def Value(self, value: Path):
620 """
621 Set the internal path object.
623 :param value: Value to set.
624 :raises TypeError: If value is not of type :class:`~pathlib.Path`.
625 """
626 if not isinstance(value, Path): 626 ↛ 627line 626 didn't jump to line 627 because the condition on line 626 was never true
627 ex = TypeError("Value is not of type 'Path'.")
628 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
629 raise ex
631 self._path = value
633 def AsArgument(self) -> Union[str, Iterable[str]]:
634 """
635 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
636 the internal value.
638 :return: Formatted argument.
639 """
640 return f"{self._path}"
642 def __str__(self) -> str:
643 """
644 Return a string representation of this argument instance.
646 :return: Argument formatted and enclosed in double quotes.
647 """
648 return f"\"{self._path}\""
650 __repr__ = __str__
653@export
654class PathListArgument(CommandLineArgument):
655 """
656 Represents a list of path arguments (:class:`~pyTooling.CLIAbstraction.Argument.PathArgument`).
657 """
658 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`.
659 _paths: List[Path]
661 def __init__(self, paths: Iterable[Path]) -> None:
662 """
663 Initializes a PathListArgument instance.
665 :param paths: An iterable os Path instances.
666 :raises TypeError: If iterable parameter 'paths' contains elements not of type :class:`~pathlib.Path`.
667 """
668 self._paths = []
669 for path in paths:
670 if not isinstance(path, Path): 670 ↛ 671line 670 didn't jump to line 671 because the condition on line 670 was never true
671 ex = TypeError(f"Parameter 'paths' contains elements which are not of type 'Path'.")
672 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
673 raise ex
675 self._paths.append(path)
677 @property
678 def Value(self) -> List[Path]:
679 """
680 Get the internal list of path objects.
682 :return: Reference to the internal list of path objects.
683 """
684 return self._paths
686 @Value.setter
687 def Value(self, value: Iterable[Path]):
688 """
689 Overwrite all elements in the internal list of path objects.
691 .. note:: The list object is not replaced, but cleared and then reused by adding the given elements in the iterable.
693 :param value: List of path objects to set.
694 :raises TypeError: If value contains elements, which are not of type :class:`~pathlib.Path`.
695 """
696 self._paths.clear()
697 for path in value:
698 if not isinstance(path, Path):
699 ex = TypeError(f"Value contains elements which are not of type 'Path'.")
700 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
701 raise ex
702 self._paths.append(path)
704 def AsArgument(self) -> Union[str, Iterable[str]]:
705 """
706 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
707 the internal value.
709 :return: Sequence of formatted arguments.
710 """
711 return [f"{path}" for path in self._paths]
713 def __str__(self) -> str:
714 """
715 Return a string representation of this argument instance.
717 :return: Space separated sequence of arguments formatted and each enclosed in double quotes.
718 """
719 return " ".join([f"\"{value}\"" for value in self.AsArgument()])
721 def __repr__(self) -> str:
722 """
723 Return a string representation of this argument instance.
725 :return: Comma separated sequence of arguments formatted and each enclosed in double quotes.
726 """
727 return ", ".join([f"\"{value}\"" for value in self.AsArgument()])