Coverage for pyTooling/CLIAbstraction/Argument.py: 91%
219 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 22:22 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 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 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 if version_info >= (3, 11): # pragma: no cover
158 ex.add_note(f"Got type '{getFullyQualifiedName(executable)}'.")
159 raise ex
161 self._executable = executable
163 @property
164 def Executable(self) -> Path:
165 """
166 Get the internal path to the wrapped executable.
168 :return: Internal path to the executable.
169 """
170 return self._executable
172 @Executable.setter
173 def Executable(self, value):
174 """
175 Set the internal path to the wrapped executable.
177 :param value: Value to path to the executable.
178 :raises TypeError: If value is not of type :class:`~pathlib.Path`.
179 """
180 if not isinstance(value, Path):
181 ex = TypeError("Parameter 'value' is not of type 'Path'.")
182 if version_info >= (3, 11): # pragma: no cover
183 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
184 raise ex
186 self._executable = value
188 def AsArgument(self) -> Union[str, Iterable[str]]:
189 """
190 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
191 the internal path to the wrapped executable.
193 :return: Formatted argument.
194 """
195 return f"{self._executable}"
197 def __str__(self) -> str:
198 """
199 Return a string representation of this argument instance.
201 :return: Argument formatted and enclosed in double quotes.
202 """
203 return f"\"{self._executable}\""
205 __repr__ = __str__
208@export
209class DelimiterArgument(CommandLineArgument, pattern="--"):
210 """
211 Represents a delimiter symbol like ``--``.
212 """
214 def __init_subclass__(cls, *args: Any, pattern: str = "--", **kwargs: Any):
215 """
216 This method is called when a class is derived.
218 :param args: Any positional arguments.
219 :param pattern: This pattern is used to format an argument. Default: ``"--"``.
220 :param kwargs: Any keyword argument.
221 """
222 kwargs["pattern"] = pattern
223 super().__init_subclass__(*args, **kwargs)
225 def AsArgument(self) -> Union[str, Iterable[str]]:
226 """
227 Convert this argument instance to a string representation with proper escaping using the matching pattern.
229 :return: Formatted argument.
230 """
231 return self._pattern
233 def __str__(self) -> str:
234 """
235 Return a string representation of this argument instance.
237 :return: Argument formatted and enclosed in double quotes.
238 """
239 return f"\"{self._pattern}\""
241 __repr__ = __str__
244@export
245class NamedArgument(CommandLineArgument, pattern="{0}"):
246 """
247 Base-class for all command line arguments with a name.
248 """
250 _name: ClassVar[str]
252 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", **kwargs: Any):
253 """
254 This method is called when a class is derived.
256 :param args: Any positional arguments.
257 :param name: Name of the argument.
258 :param pattern: This pattern is used to format an argument.
259 :param kwargs: Any keyword argument.
260 """
261 kwargs["pattern"] = pattern
262 super().__init_subclass__(*args, **kwargs)
263 cls._name = name
265 def __new__(cls, *args: Any, **kwargs: Any):
266 if cls is NamedArgument:
267 raise TypeError(f"Class '{cls.__name__}' is abstract.")
268 return super().__new__(cls, *args, **kwargs)
270 @readonly
271 def Name(self) -> str:
272 """
273 Get the internal name.
275 :return: Internal name.
276 """
277 return self._name
279 def AsArgument(self) -> Union[str, Iterable[str]]:
280 """
281 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
282 the internal name.
284 :return: Formatted argument.
285 :raises ValueError: If internal name is None.
286 """
287 if self._name is None: 287 ↛ 288line 287 didn't jump to line 288 because the condition on line 287 was never true
288 raise ValueError(f"Internal value '_name' is None.")
290 return self._pattern.format(self._name)
292 def __str__(self) -> str:
293 """
294 Return a string representation of this argument instance.
296 :return: Argument formatted and enclosed in double quotes.
297 """
298 return f"\"{self.AsArgument()}\""
300 __repr__ = __str__
303@export
304class ValuedArgument(CommandLineArgument, Generic[ValueT], pattern="{0}"):
305 """
306 Base-class for all command line arguments with a value.
307 """
309 _value: ValueT
311 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any):
312 """
313 This method is called when a class is derived.
315 :param args: Any positional arguments.
316 :param pattern: This pattern is used to format an argument.
317 :param kwargs: Any keyword argument.
318 """
319 kwargs["pattern"] = pattern
320 super().__init_subclass__(*args, **kwargs)
322 def __init__(self, value: ValueT) -> None:
323 """
324 Initializes a ValuedArgument instance.
326 :param value: Value to be stored internally.
327 :raises TypeError: If parameter 'value' is None.
328 """
329 if value is None: 329 ↛ 330line 329 didn't jump to line 330 because the condition on line 329 was never true
330 raise ValueError("Parameter 'value' is None.")
332 self._value = value
334 @property
335 def Value(self) -> ValueT:
336 """
337 Get the internal value.
339 :return: Internal value.
340 """
341 return self._value
343 @Value.setter
344 def Value(self, value: ValueT) -> None:
345 """
346 Set the internal value.
348 :param value: Value to set.
349 :raises ValueError: If value to set is None.
350 """
351 if value is None: 351 ↛ 352line 351 didn't jump to line 352 because the condition on line 351 was never true
352 raise ValueError(f"Value to set is None.")
354 self._value = value
356 def AsArgument(self) -> Union[str, Iterable[str]]:
357 """
358 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
359 the internal value.
361 :return: Formatted argument.
362 """
363 return self._pattern.format(self._value)
365 def __str__(self) -> str:
366 """
367 Return a string representation of this argument instance.
369 :return: Argument formatted and enclosed in double quotes.
370 """
371 return f"\"{self.AsArgument()}\""
373 __repr__ = __str__
376class NamedAndValuedArgument(NamedArgument, ValuedArgument, Generic[ValueT], pattern="{0}={1}"):
377 """
378 Base-class for all command line arguments with a name and a value.
379 """
381 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}={1}", **kwargs: Any):
382 """
383 This method is called when a class is derived.
385 :param args: Any positional arguments.
386 :param name: Name of the argument.
387 :param pattern: This pattern is used to format an argument.
388 :param kwargs: Any keyword argument.
389 """
390 kwargs["name"] = name
391 kwargs["pattern"] = pattern
392 super().__init_subclass__(*args, **kwargs)
393 del kwargs["name"]
394 del kwargs["pattern"]
395 ValuedArgument.__init_subclass__(*args, **kwargs)
397 def __init__(self, value: ValueT) -> None:
398 ValuedArgument.__init__(self, value)
400 def AsArgument(self) -> Union[str, Iterable[str]]:
401 """
402 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
403 the internal name and value.
405 :return: Formatted argument.
406 :raises ValueError: If internal name is None.
407 """
408 if self._name is None: 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true
409 raise ValueError(f"Internal value '_name' is None.")
411 return self._pattern.format(self._name, self._value)
413 def __str__(self) -> str:
414 """
415 Return a string representation of this argument instance.
417 :return: Argument formatted and enclosed in double quotes.
418 """
419 return f"\"{self.AsArgument()}\""
421 __repr__ = __str__
424class NamedTupledArgument(NamedArgument, ValuedArgument, Generic[ValueT], pattern="{0}"):
425 """
426 Class and base-class for all TupleFlag classes, which represents an argument with separate value.
428 A tuple argument is a command line argument followed by a separate value. Name and value are passed as two arguments
429 to the executable.
431 **Example: **
433 * `width 100``
434 """
436 _valuePattern: ClassVar[str]
438 def __init_subclass__(cls, *args: Any, name: Nullable[str] = None, pattern: str = "{0}", valuePattern: str = "{0}", **kwargs: Any):
439 kwargs["name"] = name
440 kwargs["pattern"] = pattern
441 super().__init_subclass__(*args, **kwargs)
442 cls._valuePattern = valuePattern
444 def __new__(cls, *args: Any, **kwargs: Any):
445 if cls is NamedTupledArgument:
446 raise TypeError(f"Class '{cls.__name__}' is abstract.")
447 return super().__new__(cls, *args, **kwargs)
449 def __init__(self, value: ValueT) -> None:
450 ValuedArgument.__init__(self, value)
452 # TODO: Add property to read value pattern
454 # @property
455 # def ValuePattern(self) -> str:
456 # if self._valuePattern is None:
457 # raise ValueError(f"") # XXX: add message
458 #
459 # return self._valuePattern
461 def AsArgument(self) -> Union[str, Iterable[str]]:
462 """
463 Convert this argument instance to a sequence of string representations with proper escaping using the matching
464 pattern based on the internal name and value.
466 :return: Formatted argument as tuple of strings.
467 :raises ValueError: If internal name is None.
468 """
469 if self._name is None: 469 ↛ 470line 469 didn't jump to line 470 because the condition on line 469 was never true
470 raise ValueError(f"Internal value '_name' is None.")
472 return (
473 self._pattern.format(self._name),
474 self._valuePattern.format(self._value)
475 )
477 def __str__(self) -> str:
478 """
479 Return a string representation of this argument instance.
481 :return: Space separated sequence of arguments formatted and each enclosed in double quotes.
482 """
483 return " ".join([f"\"{item}\"" for item in self.AsArgument()])
485 def __repr__(self) -> str:
486 """
487 Return a string representation of this argument instance.
489 :return: Comma separated sequence of arguments formatted and each enclosed in double quotes.
490 """
491 return ", ".join([f"\"{item}\"" for item in self.AsArgument()])
494@export
495class StringArgument(ValuedArgument, pattern="{0}"):
496 """
497 Represents a simple string argument.
499 A list of strings is available as :class:`~pyTooling.CLIAbstraction.Argument.StringListArgument`.
500 """
502 def __init_subclass__(cls, *args: Any, pattern: str = "{0}", **kwargs: Any):
503 """
504 This method is called when a class is derived.
506 :param args: Any positional arguments.
507 :param pattern: This pattern is used to format an argument.
508 :param kwargs: Any keyword argument.
509 """
510 kwargs["pattern"] = pattern
511 super().__init_subclass__(*args, **kwargs)
514@export
515class StringListArgument(ValuedArgument):
516 """
517 Represents a list of string argument (:class:`~pyTooling.CLIAbstraction.Argument.StringArgument`)."""
519 def __init__(self, values: Iterable[str]) -> None:
520 """
521 Initializes a StringListArgument instance.
523 :param values: An iterable of str instances.
524 :raises TypeError: If iterable parameter 'values' contains elements not of type :class:`str`.
525 """
526 self._values = []
527 for value in values:
528 if not isinstance(value, str): 528 ↛ 529line 528 didn't jump to line 529 because the condition on line 528 was never true
529 ex = TypeError(f"Parameter 'values' contains elements which are not of type 'str'.")
530 if version_info >= (3, 11): # pragma: no cover
531 ex.add_note(f"Got type '{getFullyQualifiedName(values)}'.")
532 raise ex
534 self._values.append(value)
536 @property
537 def Value(self) -> List[str]:
538 """
539 Get the internal list of str objects.
541 :return: Reference to the internal list of str objects.
542 """
543 return self._values
545 @Value.setter
546 def Value(self, value: Iterable[str]):
547 """
548 Overwrite all elements in the internal list of str objects.
550 .. note:: The list object is not replaced, but cleared and then reused by adding the given elements in the iterable.
552 :param value: List of str objects to set.
553 :raises TypeError: If value contains elements, which are not of type :class:`str`.
554 """
555 self._values.clear()
556 for value in value:
557 if not isinstance(value, str):
558 ex = TypeError(f"Value contains elements which are not of type 'str'.")
559 if version_info >= (3, 11): # pragma: no cover
560 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
561 raise ex
562 self._values.append(value)
564 def AsArgument(self) -> Union[str, Iterable[str]]:
565 """
566 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
567 the internal value.
569 :return: Sequence of formatted arguments.
570 """
571 return [f"{value}" for value in self._values]
573 def __str__(self) -> str:
574 """
575 Return a string representation of this argument instance.
577 :return: Space separated sequence of arguments formatted and each enclosed in double quotes.
578 """
579 return " ".join([f"\"{value}\"" for value in self.AsArgument()])
581 def __repr__(self) -> str:
582 """
583 Return a string representation of this argument instance.
585 :return: Comma separated sequence of arguments formatted and each enclosed in double quotes.
586 """
587 return ", ".join([f"\"{value}\"" for value in self.AsArgument()])
590# TODO: Add option to class if path should be checked for existence
591@export
592class PathArgument(CommandLineArgument):
593 """
594 Represents a single path argument.
596 A list of paths is available as :class:`~pyTooling.CLIAbstraction.Argument.PathListArgument`.
597 """
598 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`.
599 _path: Path
601 def __init__(self, path: Path) -> None:
602 """
603 Initializes a PathArgument instance.
605 :param path: Path to a filesystem object.
606 :raises TypeError: If parameter 'path' is not of type :class:`~pathlib.Path`.
607 """
608 if not isinstance(path, Path): 608 ↛ 609line 608 didn't jump to line 609 because the condition on line 608 was never true
609 ex = TypeError("Parameter 'path' is not of type 'Path'.")
610 if version_info >= (3, 11): # pragma: no cover
611 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
612 raise ex
613 self._path = path
615 @property
616 def Value(self) -> Path:
617 """
618 Get the internal path object.
620 :return: Internal path object.
621 """
622 return self._path
624 @Value.setter
625 def Value(self, value: Path):
626 """
627 Set the internal path object.
629 :param value: Value to set.
630 :raises TypeError: If value is not of type :class:`~pathlib.Path`.
631 """
632 if not isinstance(value, Path): 632 ↛ 633line 632 didn't jump to line 633 because the condition on line 632 was never true
633 ex = TypeError("Value is not of type 'Path'.")
634 if version_info >= (3, 11): # pragma: no cover
635 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
636 raise ex
638 self._path = value
640 def AsArgument(self) -> Union[str, Iterable[str]]:
641 """
642 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
643 the internal value.
645 :return: Formatted argument.
646 """
647 return f"{self._path}"
649 def __str__(self) -> str:
650 """
651 Return a string representation of this argument instance.
653 :return: Argument formatted and enclosed in double quotes.
654 """
655 return f"\"{self._path}\""
657 __repr__ = __str__
660@export
661class PathListArgument(CommandLineArgument):
662 """
663 Represents a list of path arguments (:class:`~pyTooling.CLIAbstraction.Argument.PathArgument`).
664 """
665 # The output format can be forced to the POSIX format with :py:data:`_PosixFormat`.
666 _paths: List[Path]
668 def __init__(self, paths: Iterable[Path]) -> None:
669 """
670 Initializes a PathListArgument instance.
672 :param paths: An iterable os Path instances.
673 :raises TypeError: If iterable parameter 'paths' contains elements not of type :class:`~pathlib.Path`.
674 """
675 self._paths = []
676 for path in paths:
677 if not isinstance(path, Path): 677 ↛ 678line 677 didn't jump to line 678 because the condition on line 677 was never true
678 ex = TypeError(f"Parameter 'paths' contains elements which are not of type 'Path'.")
679 if version_info >= (3, 11): # pragma: no cover
680 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
681 raise ex
683 self._paths.append(path)
685 @property
686 def Value(self) -> List[Path]:
687 """
688 Get the internal list of path objects.
690 :return: Reference to the internal list of path objects.
691 """
692 return self._paths
694 @Value.setter
695 def Value(self, value: Iterable[Path]):
696 """
697 Overwrite all elements in the internal list of path objects.
699 .. note:: The list object is not replaced, but cleared and then reused by adding the given elements in the iterable.
701 :param value: List of path objects to set.
702 :raises TypeError: If value contains elements, which are not of type :class:`~pathlib.Path`.
703 """
704 self._paths.clear()
705 for path in value:
706 if not isinstance(path, Path):
707 ex = TypeError(f"Value contains elements which are not of type 'Path'.")
708 if version_info >= (3, 11): # pragma: no cover
709 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.")
710 raise ex
711 self._paths.append(path)
713 def AsArgument(self) -> Union[str, Iterable[str]]:
714 """
715 Convert this argument instance to a string representation with proper escaping using the matching pattern based on
716 the internal value.
718 :return: Sequence of formatted arguments.
719 """
720 return [f"{path}" for path in self._paths]
722 def __str__(self) -> str:
723 """
724 Return a string representation of this argument instance.
726 :return: Space separated sequence of arguments formatted and each enclosed in double quotes.
727 """
728 return " ".join([f"\"{value}\"" for value in self.AsArgument()])
730 def __repr__(self) -> str:
731 """
732 Return a string representation of this argument instance.
734 :return: Comma separated sequence of arguments formatted and each enclosed in double quotes.
735 """
736 return ", ".join([f"\"{value}\"" for value in self.AsArgument()])