Coverage for pyTooling/CLIAbstraction/Argument.py: 90%
227 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-28 13:37 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-28 13:37 +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 sys import version_info # needed for versions before Python 3.11
40from typing import ClassVar, List, Union, Iterable, TypeVar, Generic, Any, Optional as Nullable
42try:
43 from pyTooling.Decorators import export, readonly
44 from pyTooling.Common import getFullyQualifiedName
45except (ImportError, ModuleNotFoundError): # pragma: no cover
46 print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!")
48 try:
49 from Decorators import export, readonly
50 from Common import getFullyQualifiedName
51 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
52 print("[pyTooling.Versioning] Could not import directly!")
53 raise ex
56__all__ = ["ValueT"]
59ValueT = TypeVar("ValueT") #: The type of value in a valued argument.
62@export
63class CommandLineArgument:
64 """
65 Base-class for all *Argument* classes.
67 An argument instance can be converted via ``AsArgument`` to a single string value or a sequence of string values
68 (tuple) usable e.g. with :class:`subprocess.Popen`. Each argument class implements at least one ``pattern`` parameter
69 to specify how argument are formatted.
71 There are multiple derived formats supporting:
73 * commands |br|
74 |rarr| :mod:`~pyTooling.CLIAbstraction.Command`
75 * simple names (flags) |br|
76 |rarr| :mod:`~pyTooling.CLIAbstraction.Flag`, :mod:`~pyTooling.CLIAbstraction.BooleanFlag`
77 * simple values (vlaued flags) |br|
78 |rarr| :class:`~pyTooling.CLIAbstraction.Argument.StringArgument`, :class:`~pyTooling.CLIAbstraction.Argument.PathArgument`
79 * names and values |br|
80 |rarr| :mod:`~pyTooling.CLIAbstraction.ValuedFlag`, :mod:`~pyTooling.CLIAbstraction.OptionalValuedFlag`
81 * key-value pairs |br|
82 |rarr| :mod:`~pyTooling.CLIAbstraction.NamedKeyValuePair`
83 """
85 _pattern: ClassVar[str]
87 def __init_subclass__(cls, *args: Any, pattern: Nullable[str] = None, **kwargs: Any):
88 """This method is called when a class is derived.
90 :param args: Any positional arguments.
91 :param pattern: This pattern is used to format an argument.
92 :param kwargs: Any keyword argument.
93 """
94 super().__init_subclass__(*args, **kwargs)
95 cls._pattern = pattern
97 def __new__(cls, *args: Any, **kwargs: Any):
98 if cls is CommandLineArgument:
99 raise TypeError(f"Class '{cls.__name__}' is abstract.")
101 # TODO: not sure why parameters meant for __init__ do reach this level and distract __new__ from it's work
102 return super().__new__(cls)
104 # TODO: Add property to read pattern
106 @abstractmethod
107 def AsArgument(self) -> Union[str, Iterable[str]]: # type: ignore[empty-body]
108 """
109 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
110 the internal name and value.
112 :return: Formatted argument.
113 :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass.
114 """
115 raise NotImplementedError(f"Method 'AsArgument' is an abstract method and must be implemented by a subclass.")
117 @abstractmethod
118 def __str__(self) -> str: # type: ignore[empty-body]
119 """
120 Return a string representation of this argument instance.
122 :return: Argument formatted and enclosed in double quotes.
123 :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass.
124 """
125 raise NotImplementedError(f"Method '__str__' is an abstract method and must be implemented by a subclass.")
127 @abstractmethod
128 def __repr__(self) -> str: # type: ignore[empty-body]
129 """
130 Return a string representation of this argument instance.
132 .. note:: By default, this method is identical to :meth:`__str__`.
134 :return: Argument formatted and enclosed in double quotes.
135 :raises NotImplementedError: This is an abstract method and must be overwritten by a subclass.
136 """
137 raise NotImplementedError(f"Method '__repr__' is an abstract method and must be implemented by a subclass.")
140@export
141class ExecutableArgument(CommandLineArgument):
142 """
143 Represents the executable.
144 """
146 _executable: Path
148 def __init__(self, executable: Path) -> None:
149 """
150 Initializes a ExecutableArgument instance.
152 :param executable: Path to the executable.
153 :raises TypeError: If parameter 'executable' is not of type :class:`~pathlib.Path`.
154 """
155 if not isinstance(executable, Path):
156 ex = TypeError("Parameter 'executable' is not of type 'Path'.")
157 ex.add_note(f"Got type '{getFullyQualifiedName(executable)}'.")
158 raise ex
160 self._executable = executable
162 @property
163 def Executable(self) -> Path:
164 """
165 Get the internal path to the wrapped executable.
167 :return: Internal path to the executable.
168 """
169 return self._executable
171 @Executable.setter
172 def Executable(self, value):
173 """
174 Set the internal path to the wrapped executable.
176 :param value: Value to path to the executable.
177 :raises TypeError: If value is not of type :class:`~pathlib.Path`.
178 """
179 if not isinstance(value, Path):
180 ex = TypeError("Parameter 'value' is not of type 'Path'.")
181 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
182 raise ex
184 self._executable = value
186 def AsArgument(self) -> Union[str, Iterable[str]]:
187 """
188 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
189 the internal path to the wrapped executable.
191 :return: Formatted argument.
192 """
193 return f"{self._executable}"
195 def __str__(self) -> str:
196 """
197 Return a string representation of this argument instance.
199 :return: Argument formatted and enclosed in double quotes.
200 """
201 return f"\"{self._executable}\""
203 __repr__ = __str__
206@export
207class DelimiterArgument(CommandLineArgument, pattern="--"):
208 """
209 Represents a delimiter symbol like ``--``.
210 """
212 def __init_subclass__(cls, *args: Any, pattern: str = "--", **kwargs: Any):
213 """
214 This method is called when a class is derived.
216 :param args: Any positional arguments.
217 :param pattern: This pattern is used to format an argument. Default: ``"--"``.
218 :param kwargs: Any keyword argument.
219 """
220 kwargs["pattern"] = pattern
221 super().__init_subclass__(*args, **kwargs)
223 def AsArgument(self) -> Union[str, Iterable[str]]:
224 """
225 Convert this argument instance to a string representation with proper escaping using the matching pattern.
227 :return: Formatted argument.
228 """
229 return self._pattern
231 def __str__(self) -> str:
232 """
233 Return a string representation of this argument instance.
235 :return: Argument formatted and enclosed in double quotes.
236 """
237 return f"\"{self._pattern}\""
239 __repr__ = __str__
242@export
243class NamedArgument(CommandLineArgument, pattern="{0}"):
244 """
245 Base-class for all command line arguments with a name.
246 """
248 _name: ClassVar[str]
250 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", **kwargs: Any):
251 """
252 This method is called when a class is derived.
254 :param args: Any positional arguments.
255 :param name: Name of the argument.
256 :param pattern: This pattern is used to format an argument.
257 :param kwargs: Any keyword argument.
258 """
259 kwargs["pattern"] = pattern
260 super().__init_subclass__(*args, **kwargs)
261 cls._name = name
263 def __new__(cls, *args: Any, **kwargs: Any):
264 if cls is NamedArgument:
265 raise TypeError(f"Class '{cls.__name__}' is abstract.")
266 return super().__new__(cls, *args, **kwargs)
268 @readonly
269 def Name(self) -> str:
270 """
271 Get the internal name.
273 :return: Internal name.
274 """
275 return self._name
277 def AsArgument(self) -> Union[str, Iterable[str]]:
278 """
279 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
280 the internal name.
282 :return: Formatted argument.
283 :raises ValueError: If internal name is None.
284 """
285 if self._name is None: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true
286 raise ValueError(f"Internal value '_name' is None.")
288 return self._pattern.format(self._name)
290 def __str__(self) -> str:
291 """
292 Return a string representation of this argument instance.
294 :return: Argument formatted and enclosed in double quotes.
295 """
296 return f"\"{self.AsArgument()}\""
298 __repr__ = __str__
301@export
302class ValuedArgument(CommandLineArgument, Generic[ValueT], pattern="{0}"):
303 """
304 Base-class for all command line arguments with a value.
305 """
307 _value: ValueT
309 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any):
310 """
311 This method is called when a class is derived.
313 :param args: Any positional arguments.
314 :param pattern: This pattern is used to format an argument.
315 :param kwargs: Any keyword argument.
316 """
317 kwargs["pattern"] = pattern
318 super().__init_subclass__(*args, **kwargs)
320 def __init__(self, value: ValueT) -> None:
321 """
322 Initializes a ValuedArgument instance.
324 :param value: Value to be stored internally.
325 :raises TypeError: If parameter 'value' is None.
326 """
327 if value is None: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true
328 raise ValueError("Parameter 'value' is None.")
330 self._value = value
332 @property
333 def Value(self) -> ValueT:
334 """
335 Get the internal value.
337 :return: Internal value.
338 """
339 return self._value
341 @Value.setter
342 def Value(self, value: ValueT) -> None:
343 """
344 Set the internal value.
346 :param value: Value to set.
347 :raises ValueError: If value to set is None.
348 """
349 if value is None: 349 ↛ 350line 349 didn't jump to line 350 because the condition on line 349 was never true
350 raise ValueError(f"Value to set is None.")
352 self._value = value
354 def AsArgument(self) -> Union[str, Iterable[str]]:
355 """
356 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
357 the internal value.
359 :return: Formatted argument.
360 """
361 return self._pattern.format(self._value)
363 def __str__(self) -> str:
364 """
365 Return a string representation of this argument instance.
367 :return: Argument formatted and enclosed in double quotes.
368 """
369 return f"\"{self.AsArgument()}\""
371 __repr__ = __str__
374class NamedAndValuedArgument(NamedArgument, ValuedArgument, Generic[ValueT], pattern="{0}={1}"):
375 """
376 Base-class for all command line arguments with a name and a value.
377 """
379 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}={1}", **kwargs: Any):
380 """
381 This method is called when a class is derived.
383 :param args: Any positional arguments.
384 :param name: Name of the argument.
385 :param pattern: This pattern is used to format an argument.
386 :param kwargs: Any keyword argument.
387 """
388 kwargs["name"] = name
389 kwargs["pattern"] = pattern
390 super().__init_subclass__(*args, **kwargs)
391 del kwargs["name"]
392 del kwargs["pattern"]
393 ValuedArgument.__init_subclass__(*args, **kwargs)
395 def __init__(self, value: ValueT) -> None:
396 ValuedArgument.__init__(self, value)
398 def AsArgument(self) -> Union[str, Iterable[str]]:
399 """
400 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
401 the internal name and value.
403 :return: Formatted argument.
404 :raises ValueError: If internal name is None.
405 """
406 if self._name is None: 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true
407 raise ValueError(f"Internal value '_name' is None.")
409 return self._pattern.format(self._name, self._value)
411 def __str__(self) -> str:
412 """
413 Return a string representation of this argument instance.
415 :return: Argument formatted and enclosed in double quotes.
416 """
417 return f"\"{self.AsArgument()}\""
419 __repr__ = __str__
422class NamedTupledArgument(NamedArgument, ValuedArgument, Generic[ValueT], pattern="{0}"):
423 """
424 Class and base-class for all TupleFlag classes, which represents an argument with separate value.
426 A tuple argument is a command line argument followed by a separate value. Name and value are passed as two arguments
427 to the executable.
429 **Example: **
431 * `width 100``
432 """
434 _valuePattern: ClassVar[str]
436 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", valuePattern: str = "{0}", **kwargs: Any):
437 kwargs["name"] = name
438 kwargs["pattern"] = pattern
439 super().__init_subclass__(*args, **kwargs)
440 cls._valuePattern = valuePattern
442 def __new__(cls, *args: Any, **kwargs: Any):
443 if cls is NamedTupledArgument:
444 raise TypeError(f"Class '{cls.__name__}' is abstract.")
445 return super().__new__(cls, *args, **kwargs)
447 def __init__(self, value: ValueT) -> None:
448 ValuedArgument.__init__(self, value)
450 # TODO: Add property to read value pattern
452 # @property
453 # def ValuePattern(self) -> str:
454 # if self._valuePattern is None:
455 # raise ValueError(f"") # XXX: add message
456 #
457 # return self._valuePattern
459 def AsArgument(self) -> Union[str, Iterable[str]]:
460 """
461 Convert this argument instance to a sequence of string representations with proper escaping using the matching
462 pattern based on the internal name and value.
464 :return: Formatted argument as tuple of strings.
465 :raises ValueError: If internal name is None.
466 """
467 if self._name is None: 467 ↛ 468line 467 didn't jump to line 468 because the condition on line 467 was never true
468 raise ValueError(f"Internal value '_name' is None.")
470 return (
471 self._pattern.format(self._name),
472 self._valuePattern.format(self._value)
473 )
475 def __str__(self) -> str:
476 """
477 Return a string representation of this argument instance.
479 :return: Space separated sequence of arguments formatted and each enclosed in double quotes.
480 """
481 return " ".join([f"\"{item}\"" for item in self.AsArgument()])
483 def __repr__(self) -> str:
484 """
485 Return a string representation of this argument instance.
487 :return: Comma separated sequence of arguments formatted and each enclosed in double quotes.
488 """
489 return ", ".join([f"\"{item}\"" for item in self.AsArgument()])
492@export
493class StringArgument(ValuedArgument, pattern="{0}"):
494 """
495 Represents a simple string argument.
497 A list of strings is available as :class:`~pyTooling.CLIAbstraction.Argument.StringListArgument`.
498 """
500 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any):
501 """
502 This method is called when a class is derived.
504 :param args: Any positional arguments.
505 :param pattern: This pattern is used to format an argument.
506 :param kwargs: Any keyword argument.
507 """
508 kwargs["pattern"] = pattern
509 super().__init_subclass__(*args, **kwargs)
512@export
513class StringListArgument(ValuedArgument):
514 """
515 Represents a list of string argument (:class:`~pyTooling.CLIAbstraction.Argument.StringArgument`)."""
517 def __init__(self, values: Iterable[str]) -> None:
518 """
519 Initializes a StringListArgument instance.
521 :param values: An iterable of str instances.
522 :raises TypeError: If iterable parameter 'values' contains elements not of type :class:`str`.
523 """
524 self._values = []
525 for value in values:
526 if not isinstance(value, str): 526 ↛ 527line 526 didn't jump to line 527 because the condition on line 526 was never true
527 ex = TypeError(f"Parameter 'values' contains elements which are not of type 'str'.")
528 ex.add_note(f"Got type '{getFullyQualifiedName(values)}'.")
529 raise ex
531 self._values.append(value)
533 @property
534 def Value(self) -> List[str]:
535 """
536 Get the internal list of str objects.
538 :return: Reference to the internal list of str objects.
539 """
540 return self._values
542 @Value.setter
543 def Value(self, value: Iterable[str]):
544 """
545 Overwrite all elements in the internal list of str objects.
547 .. note:: The list object is not replaced, but cleared and then reused by adding the given elements in the iterable.
549 :param value: List of str objects to set.
550 :raises TypeError: If value contains elements, which are not of type :class:`str`.
551 """
552 self._values.clear()
553 for value in value:
554 if not isinstance(value, str):
555 ex = TypeError(f"Value contains elements which are not of type 'str'.")
556 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
557 raise ex
558 self._values.append(value)
560 def AsArgument(self) -> Union[str, Iterable[str]]:
561 """
562 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
563 the internal value.
565 :return: Sequence of formatted arguments.
566 """
567 return [f"{value}" for value in self._values]
569 def __str__(self) -> str:
570 """
571 Return a string representation of this argument instance.
573 :return: Space separated sequence of arguments formatted and each enclosed in double quotes.
574 """
575 return " ".join([f"\"{value}\"" for value in self.AsArgument()])
577 def __repr__(self) -> str:
578 """
579 Return a string representation of this argument instance.
581 :return: Comma separated sequence of arguments formatted and each enclosed in double quotes.
582 """
583 return ", ".join([f"\"{value}\"" for value in self.AsArgument()])
586# TODO: Add option to class if path should be checked for existence
587@export
588class PathArgument(CommandLineArgument):
589 """
590 Represents a single path argument.
592 A list of paths is available as :class:`~pyTooling.CLIAbstraction.Argument.PathListArgument`.
593 """
594 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`.
595 _path: Path
597 def __init__(self, path: Path) -> None:
598 """
599 Initializes a PathArgument instance.
601 :param path: Path to a filesystem object.
602 :raises TypeError: If parameter 'path' is not of type :class:`~pathlib.Path`.
603 """
604 if not isinstance(path, Path): 604 ↛ 605line 604 didn't jump to line 605 because the condition on line 604 was never true
605 ex = TypeError("Parameter 'path' is not of type 'Path'.")
606 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
607 raise ex
608 self._path = path
610 @property
611 def Value(self) -> Path:
612 """
613 Get the internal path object.
615 :return: Internal path object.
616 """
617 return self._path
619 @Value.setter
620 def Value(self, value: Path):
621 """
622 Set the internal path object.
624 :param value: Value to set.
625 :raises TypeError: If value is not of type :class:`~pathlib.Path`.
626 """
627 if not isinstance(value, Path): 627 ↛ 628line 627 didn't jump to line 628 because the condition on line 627 was never true
628 ex = TypeError("Value is not of type 'Path'.")
629 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
630 raise ex
632 self._path = value
634 def AsArgument(self) -> Union[str, Iterable[str]]:
635 """
636 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
637 the internal value.
639 :return: Formatted argument.
640 """
641 return f"{self._path}"
643 def __str__(self) -> str:
644 """
645 Return a string representation of this argument instance.
647 :return: Argument formatted and enclosed in double quotes.
648 """
649 return f"\"{self._path}\""
651 __repr__ = __str__
654@export
655class PathListArgument(CommandLineArgument):
656 """
657 Represents a list of path arguments (:class:`~pyTooling.CLIAbstraction.Argument.PathArgument`).
658 """
659 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`.
660 _paths: List[Path]
662 def __init__(self, paths: Iterable[Path]) -> None:
663 """
664 Initializes a PathListArgument instance.
666 :param paths: An iterable os Path instances.
667 :raises TypeError: If iterable parameter 'paths' contains elements not of type :class:`~pathlib.Path`.
668 """
669 self._paths = []
670 for path in paths:
671 if not isinstance(path, Path): 671 ↛ 672line 671 didn't jump to line 672 because the condition on line 671 was never true
672 ex = TypeError(f"Parameter 'paths' contains elements which are not of type 'Path'.")
673 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
674 raise ex
676 self._paths.append(path)
678 @property
679 def Value(self) -> List[Path]:
680 """
681 Get the internal list of path objects.
683 :return: Reference to the internal list of path objects.
684 """
685 return self._paths
687 @Value.setter
688 def Value(self, value: Iterable[Path]):
689 """
690 Overwrite all elements in the internal list of path objects.
692 .. note:: The list object is not replaced, but cleared and then reused by adding the given elements in the iterable.
694 :param value: List of path objects to set.
695 :raises TypeError: If value contains elements, which are not of type :class:`~pathlib.Path`.
696 """
697 self._paths.clear()
698 for path in value:
699 if not isinstance(path, Path):
700 ex = TypeError(f"Value contains elements which are not of type 'Path'.")
701 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
702 raise ex
703 self._paths.append(path)
705 def AsArgument(self) -> Union[str, Iterable[str]]:
706 """
707 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
708 the internal value.
710 :return: Sequence of formatted arguments.
711 """
712 return [f"{path}" for path in self._paths]
714 def __str__(self) -> str:
715 """
716 Return a string representation of this argument instance.
718 :return: Space separated sequence of arguments formatted and each enclosed in double quotes.
719 """
720 return " ".join([f"\"{value}\"" for value in self.AsArgument()])
722 def __repr__(self) -> str:
723 """
724 Return a string representation of this argument instance.
726 :return: Comma separated sequence of arguments formatted and each enclosed in double quotes.
727 """
728 return ", ".join([f"\"{value}\"" for value in self.AsArgument()])