click-extended 1.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- click_extended/core/__init__.py +10 -0
- click_extended/core/decorators/__init__.py +21 -0
- click_extended/core/decorators/argument.py +227 -0
- click_extended/core/decorators/command.py +93 -0
- click_extended/core/decorators/env.py +155 -0
- click_extended/core/decorators/group.py +96 -0
- click_extended/core/decorators/option.py +347 -0
- click_extended/core/decorators/prompt.py +69 -0
- click_extended/core/decorators/selection.py +155 -0
- click_extended/core/decorators/tag.py +109 -0
- click_extended/core/nodes/__init__.py +21 -0
- click_extended/core/nodes/_root_node.py +1012 -0
- click_extended/core/nodes/argument_node.py +165 -0
- click_extended/core/nodes/child_node.py +555 -0
- click_extended/core/nodes/child_validation_node.py +100 -0
- click_extended/core/nodes/node.py +55 -0
- click_extended/core/nodes/option_node.py +205 -0
- click_extended/core/nodes/parent_node.py +220 -0
- click_extended/core/nodes/validation_node.py +124 -0
- click_extended/core/other/__init__.py +7 -0
- click_extended/core/other/_click_command.py +60 -0
- click_extended/core/other/_click_group.py +246 -0
- click_extended/core/other/_tree.py +491 -0
- click_extended/core/other/context.py +496 -0
- click_extended/decorators/__init__.py +29 -0
- click_extended/decorators/check/__init__.py +57 -0
- click_extended/decorators/check/conflicts.py +149 -0
- click_extended/decorators/check/contains.py +69 -0
- click_extended/decorators/check/dependencies.py +115 -0
- click_extended/decorators/check/divisible_by.py +48 -0
- click_extended/decorators/check/ends_with.py +85 -0
- click_extended/decorators/check/exclusive.py +75 -0
- click_extended/decorators/check/falsy.py +37 -0
- click_extended/decorators/check/is_email.py +43 -0
- click_extended/decorators/check/is_hex_color.py +41 -0
- click_extended/decorators/check/is_hostname.py +47 -0
- click_extended/decorators/check/is_ipv4.py +46 -0
- click_extended/decorators/check/is_ipv6.py +46 -0
- click_extended/decorators/check/is_json.py +40 -0
- click_extended/decorators/check/is_mac_address.py +40 -0
- click_extended/decorators/check/is_negative.py +37 -0
- click_extended/decorators/check/is_non_zero.py +37 -0
- click_extended/decorators/check/is_port.py +39 -0
- click_extended/decorators/check/is_positive.py +37 -0
- click_extended/decorators/check/is_url.py +75 -0
- click_extended/decorators/check/is_uuid.py +40 -0
- click_extended/decorators/check/length.py +68 -0
- click_extended/decorators/check/not_empty.py +49 -0
- click_extended/decorators/check/regex.py +47 -0
- click_extended/decorators/check/requires.py +190 -0
- click_extended/decorators/check/starts_with.py +87 -0
- click_extended/decorators/check/truthy.py +37 -0
- click_extended/decorators/compare/__init__.py +15 -0
- click_extended/decorators/compare/at_least.py +57 -0
- click_extended/decorators/compare/at_most.py +57 -0
- click_extended/decorators/compare/between.py +119 -0
- click_extended/decorators/compare/greater_than.py +183 -0
- click_extended/decorators/compare/less_than.py +183 -0
- click_extended/decorators/convert/__init__.py +31 -0
- click_extended/decorators/convert/convert_angle.py +94 -0
- click_extended/decorators/convert/convert_area.py +123 -0
- click_extended/decorators/convert/convert_bits.py +211 -0
- click_extended/decorators/convert/convert_distance.py +154 -0
- click_extended/decorators/convert/convert_energy.py +155 -0
- click_extended/decorators/convert/convert_power.py +128 -0
- click_extended/decorators/convert/convert_pressure.py +131 -0
- click_extended/decorators/convert/convert_speed.py +122 -0
- click_extended/decorators/convert/convert_temperature.py +89 -0
- click_extended/decorators/convert/convert_time.py +108 -0
- click_extended/decorators/convert/convert_volume.py +218 -0
- click_extended/decorators/convert/convert_weight.py +158 -0
- click_extended/decorators/load/__init__.py +13 -0
- click_extended/decorators/load/load_csv.py +117 -0
- click_extended/decorators/load/load_json.py +61 -0
- click_extended/decorators/load/load_toml.py +47 -0
- click_extended/decorators/load/load_yaml.py +72 -0
- click_extended/decorators/math/__init__.py +37 -0
- click_extended/decorators/math/absolute.py +35 -0
- click_extended/decorators/math/add.py +48 -0
- click_extended/decorators/math/ceil.py +36 -0
- click_extended/decorators/math/clamp.py +51 -0
- click_extended/decorators/math/divide.py +42 -0
- click_extended/decorators/math/floor.py +36 -0
- click_extended/decorators/math/maximum.py +39 -0
- click_extended/decorators/math/minimum.py +39 -0
- click_extended/decorators/math/modulo.py +39 -0
- click_extended/decorators/math/multiply.py +51 -0
- click_extended/decorators/math/normalize.py +76 -0
- click_extended/decorators/math/power.py +39 -0
- click_extended/decorators/math/rounded.py +39 -0
- click_extended/decorators/math/sqrt.py +39 -0
- click_extended/decorators/math/subtract.py +39 -0
- click_extended/decorators/math/to_percent.py +63 -0
- click_extended/decorators/misc/__init__.py +17 -0
- click_extended/decorators/misc/choice.py +139 -0
- click_extended/decorators/misc/confirm_if.py +147 -0
- click_extended/decorators/misc/default.py +95 -0
- click_extended/decorators/misc/deprecated.py +131 -0
- click_extended/decorators/misc/experimental.py +79 -0
- click_extended/decorators/misc/now.py +42 -0
- click_extended/decorators/random/__init__.py +21 -0
- click_extended/decorators/random/random_bool.py +49 -0
- click_extended/decorators/random/random_choice.py +63 -0
- click_extended/decorators/random/random_datetime.py +140 -0
- click_extended/decorators/random/random_float.py +62 -0
- click_extended/decorators/random/random_integer.py +56 -0
- click_extended/decorators/random/random_prime.py +196 -0
- click_extended/decorators/random/random_string.py +77 -0
- click_extended/decorators/random/random_uuid.py +119 -0
- click_extended/decorators/transform/__init__.py +71 -0
- click_extended/decorators/transform/add_prefix.py +58 -0
- click_extended/decorators/transform/add_suffix.py +58 -0
- click_extended/decorators/transform/apply.py +35 -0
- click_extended/decorators/transform/basename.py +44 -0
- click_extended/decorators/transform/dirname.py +44 -0
- click_extended/decorators/transform/expand_vars.py +36 -0
- click_extended/decorators/transform/remove_prefix.py +57 -0
- click_extended/decorators/transform/remove_suffix.py +57 -0
- click_extended/decorators/transform/replace.py +46 -0
- click_extended/decorators/transform/slugify.py +45 -0
- click_extended/decorators/transform/split.py +43 -0
- click_extended/decorators/transform/strip.py +148 -0
- click_extended/decorators/transform/to_case.py +216 -0
- click_extended/decorators/transform/to_date.py +75 -0
- click_extended/decorators/transform/to_datetime.py +83 -0
- click_extended/decorators/transform/to_path.py +274 -0
- click_extended/decorators/transform/to_time.py +77 -0
- click_extended/decorators/transform/to_timestamp.py +114 -0
- click_extended/decorators/transform/truncate.py +47 -0
- click_extended/utils/__init__.py +13 -0
- click_extended/utils/casing.py +169 -0
- click_extended/utils/checks.py +48 -0
- click_extended/utils/dispatch.py +1016 -0
- click_extended/utils/format.py +101 -0
- click_extended/utils/humanize.py +209 -0
- click_extended/utils/naming.py +238 -0
- click_extended/utils/process.py +294 -0
- click_extended/utils/selection.py +267 -0
- click_extended/utils/time.py +46 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/METADATA +2 -1
- click_extended-1.0.1.dist-info/RECORD +149 -0
- click_extended-1.0.0.dist-info/RECORD +0 -10
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/WHEEL +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/licenses/AUTHORS.md +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Child validation node module for nodes that can
|
|
3
|
+
act as both child and validation nodes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, ParamSpec, TypeVar
|
|
9
|
+
|
|
10
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
11
|
+
from click_extended.core.nodes.validation_node import ValidationNode
|
|
12
|
+
from click_extended.utils.casing import Casing
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from click_extended.types import Decorator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
P = ParamSpec("P")
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ChildValidationNode(ChildNode, ValidationNode, ABC):
|
|
23
|
+
"""
|
|
24
|
+
Base class for nodes acting as both ChildNode and ValidationNode.
|
|
25
|
+
|
|
26
|
+
ChildValidationNodes adapt their behavior based on
|
|
27
|
+
decorator placement:
|
|
28
|
+
|
|
29
|
+
- **Child Mode**: When attached to a parent node (option, argument,
|
|
30
|
+
env) or tag, the node acts as a ChildNode and processes values
|
|
31
|
+
using handler methods (handle_str, handle_int, handle_all, etc.).
|
|
32
|
+
|
|
33
|
+
- **Validation Mode**: When used standalone (not attached to a
|
|
34
|
+
parent), the node acts as a ValidationNode and runs lifecycle
|
|
35
|
+
hooks (on_init, on_finalize) at the tree level with access to
|
|
36
|
+
all parent values via context.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
```python
|
|
40
|
+
class RequiresAdmin(HybridNode):
|
|
41
|
+
def handle_all(self, value: Any, context: Context) -> Any:
|
|
42
|
+
# Called when attached to parent (child mode)
|
|
43
|
+
if not context.get("is_admin"):
|
|
44
|
+
raise ValueError("Admin access required")
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
def on_finalize(self, context: Context) -> None:
|
|
48
|
+
# Called when standalone (validation mode)
|
|
49
|
+
if not context.get("is_admin"):
|
|
50
|
+
raise ValueError("Admin access required")
|
|
51
|
+
|
|
52
|
+
# Child mode - processes 'username' value
|
|
53
|
+
@command()
|
|
54
|
+
@option("username", type=str)
|
|
55
|
+
@RequiresAdmin.as_decorator()
|
|
56
|
+
def cmd1(username: str): pass
|
|
57
|
+
|
|
58
|
+
# Validation mode - runs after all processing
|
|
59
|
+
@command()
|
|
60
|
+
@option("username", type=str)
|
|
61
|
+
@RequiresAdmin.as_decorator()
|
|
62
|
+
def cmd2(username: str): pass
|
|
63
|
+
```
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def as_decorator(cls, *args: Any, **kwargs: Any) -> "Decorator":
|
|
68
|
+
"""
|
|
69
|
+
Return a decorator representation of the child validation node.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
*args (Any):
|
|
73
|
+
Positional arguments to pass to handler/lifecycle
|
|
74
|
+
methods.
|
|
75
|
+
**kwargs (Any):
|
|
76
|
+
Keyword arguments to pass to handler/lifecycle methods.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Decorator:
|
|
80
|
+
A decorator function that registers the child
|
|
81
|
+
validation node.
|
|
82
|
+
"""
|
|
83
|
+
from click_extended.core.other._tree import Tree
|
|
84
|
+
|
|
85
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
86
|
+
"""The actual decorator that wraps the function."""
|
|
87
|
+
name = Casing.to_snake_case(cls.__name__)
|
|
88
|
+
instance = cls(name=name, process_args=args, process_kwargs=kwargs)
|
|
89
|
+
Tree.queue_child_validation(instance)
|
|
90
|
+
|
|
91
|
+
@wraps(func)
|
|
92
|
+
def wrapper(*call_args: P.args, **call_kwargs: P.kwargs) -> T:
|
|
93
|
+
return func(*call_args, **call_kwargs)
|
|
94
|
+
|
|
95
|
+
return wrapper
|
|
96
|
+
|
|
97
|
+
return decorator
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = ["ChildValidationNode"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Base Node class for all nodes in the tree structure."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Node(ABC):
|
|
7
|
+
"""Base node class for all nodes in the tree structure."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self, name: str, children: dict[str | int, "Node"] | None = None
|
|
11
|
+
) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Initialize a new `Node` instance.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
name (str):
|
|
17
|
+
The name of the node.
|
|
18
|
+
children (dict[str | int, Node] | None, optional):
|
|
19
|
+
Optional dictionary mapping child identifiers to child nodes.
|
|
20
|
+
"""
|
|
21
|
+
self.name = name
|
|
22
|
+
self._children = children if children is not None else {}
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def children(self) -> dict[str | int, "Node"]:
|
|
26
|
+
"""Get the children of this node."""
|
|
27
|
+
return self._children
|
|
28
|
+
|
|
29
|
+
@children.setter
|
|
30
|
+
def children(self, value: dict[str | int, "Node"] | None) -> None:
|
|
31
|
+
"""Set the children of this node."""
|
|
32
|
+
self._children = value if value is not None else {}
|
|
33
|
+
|
|
34
|
+
def __getitem__(self, key: str | int) -> "Node":
|
|
35
|
+
"""Get a child node by key."""
|
|
36
|
+
return self._children[key]
|
|
37
|
+
|
|
38
|
+
def __setitem__(self, key: str | int, value: "Node") -> None:
|
|
39
|
+
"""Set a child node by key."""
|
|
40
|
+
self._children[key] = value
|
|
41
|
+
|
|
42
|
+
def __len__(self) -> int:
|
|
43
|
+
"""Get the number of children."""
|
|
44
|
+
return len(self._children)
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return repr(self)
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
class_name = self.__class__.__name__
|
|
51
|
+
custom_name = self.name
|
|
52
|
+
return f"<{class_name} name='{custom_name}'>"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["Node"]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""OptionNode abstract base class for CLI option nodes."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-arguments
|
|
4
|
+
# pylint: disable=too-many-positional-arguments
|
|
5
|
+
# pylint: disable=redefined-builtin
|
|
6
|
+
# pylint: disable=arguments-differ
|
|
7
|
+
# pylint: disable=line-too-long
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, ParamSpec, Type, TypeVar
|
|
11
|
+
|
|
12
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
13
|
+
from click_extended.utils.casing import Casing
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from click_extended.core.other.context import Context
|
|
17
|
+
|
|
18
|
+
P = ParamSpec("P")
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OptionNode(ParentNode, ABC):
|
|
23
|
+
"""
|
|
24
|
+
Abstract base class for nodes that receive CLI option values.
|
|
25
|
+
|
|
26
|
+
OptionNode extends ParentNode to handle command-line options (flags).
|
|
27
|
+
The key difference is that the `load()` method receives a `value`
|
|
28
|
+
parameter containing the parsed CLI option value.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
name: str,
|
|
34
|
+
param: str | None = None,
|
|
35
|
+
short_flags: list[str] | None = None,
|
|
36
|
+
long_flags: list[str] | None = None,
|
|
37
|
+
is_flag: bool = False,
|
|
38
|
+
type: Type[Any] | None = None,
|
|
39
|
+
nargs: int = 1,
|
|
40
|
+
multiple: bool = False,
|
|
41
|
+
help: str | None = None,
|
|
42
|
+
required: bool = False,
|
|
43
|
+
default: Any = None,
|
|
44
|
+
tags: str | list[str] | None = None,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initialize a new `OptionNode` instance.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
name (str):
|
|
52
|
+
The option name (parameter name for injection).
|
|
53
|
+
param (str, optional):
|
|
54
|
+
Custom parameter name for the function.
|
|
55
|
+
If not provided, uses `name`.
|
|
56
|
+
short_flags (list[str], optional):
|
|
57
|
+
Short flags for the option (e.g., ["-p", "-P"]).
|
|
58
|
+
long_flags (list[str], optional):
|
|
59
|
+
Long flags for the option (e.g., ["--port", "--p"]).
|
|
60
|
+
If not provided, auto-generates ["--kebab-case(name)"].
|
|
61
|
+
is_flag (bool):
|
|
62
|
+
Whether this is a boolean flag (no value needed).
|
|
63
|
+
Defaults to `False`.
|
|
64
|
+
type (Type[Any], optional):
|
|
65
|
+
The type to convert the value to (`int`, `str`, `float`, `bool`).
|
|
66
|
+
nargs (int):
|
|
67
|
+
Number of arguments each occurrence accepts. Defaults to `1`.
|
|
68
|
+
multiple (bool):
|
|
69
|
+
Whether the option can be provided multiple times.
|
|
70
|
+
Defaults to `False`.
|
|
71
|
+
help (str, optional):
|
|
72
|
+
Help text for this option.
|
|
73
|
+
required (bool):
|
|
74
|
+
Whether this option is required. Defaults to `False`.
|
|
75
|
+
default (Any):
|
|
76
|
+
Default value if not provided. Defaults to `None`.
|
|
77
|
+
tags (str | list[str], optional):
|
|
78
|
+
Tag(s) to associate with this option for grouping.
|
|
79
|
+
**kwargs (Any):
|
|
80
|
+
Additional keyword arguments passed to parent class.
|
|
81
|
+
"""
|
|
82
|
+
super().__init__(
|
|
83
|
+
name=param if param is not None else name,
|
|
84
|
+
param=param,
|
|
85
|
+
help=help,
|
|
86
|
+
required=required,
|
|
87
|
+
default=default,
|
|
88
|
+
tags=tags,
|
|
89
|
+
**kwargs,
|
|
90
|
+
)
|
|
91
|
+
self.short_flags = short_flags if short_flags is not None else []
|
|
92
|
+
self.long_flags = (
|
|
93
|
+
long_flags
|
|
94
|
+
if long_flags is not None and len(long_flags) > 0
|
|
95
|
+
else [f"--{Casing.to_kebab_case(name)}"]
|
|
96
|
+
)
|
|
97
|
+
self.is_flag = is_flag
|
|
98
|
+
self.type = type
|
|
99
|
+
self.nargs = nargs
|
|
100
|
+
self.multiple = multiple
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def load(
|
|
104
|
+
self,
|
|
105
|
+
value: str | int | float | bool | None,
|
|
106
|
+
context: "Context",
|
|
107
|
+
*args: Any,
|
|
108
|
+
**kwargs: Any,
|
|
109
|
+
) -> Any:
|
|
110
|
+
"""
|
|
111
|
+
Load and process the CLI option value.
|
|
112
|
+
|
|
113
|
+
This method is called with the parsed CLI option value and should
|
|
114
|
+
return the processed value to be injected into the function.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
value (str | int | float | bool | None):
|
|
118
|
+
The parsed CLI option value from Click.
|
|
119
|
+
context (Context):
|
|
120
|
+
The current context instance.
|
|
121
|
+
*args (Any):
|
|
122
|
+
Optional positional arguments.
|
|
123
|
+
**kwargs (Any):
|
|
124
|
+
Optional keyword arguments.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Any:
|
|
128
|
+
The processed value to inject into the function.
|
|
129
|
+
"""
|
|
130
|
+
raise NotImplementedError
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def as_decorator(
|
|
134
|
+
cls,
|
|
135
|
+
*,
|
|
136
|
+
name: str,
|
|
137
|
+
param: str | None = None,
|
|
138
|
+
short_flags: list[str] | None = None,
|
|
139
|
+
long_flags: list[str] | None = None,
|
|
140
|
+
is_flag: bool = False,
|
|
141
|
+
type: Type[Any] | None = None,
|
|
142
|
+
nargs: int = 1,
|
|
143
|
+
multiple: bool = False,
|
|
144
|
+
help: str | None = None,
|
|
145
|
+
required: bool = False,
|
|
146
|
+
default: Any = None,
|
|
147
|
+
tags: str | list[str] | None = None,
|
|
148
|
+
**kwargs: Any,
|
|
149
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
150
|
+
"""
|
|
151
|
+
Return a decorator representation of the option node.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name (str):
|
|
155
|
+
The option name (parameter name for injection).
|
|
156
|
+
param (str, optional):
|
|
157
|
+
Custom parameter name for the function.
|
|
158
|
+
If not provided, uses `name`.
|
|
159
|
+
short_flags (list[str], optional):
|
|
160
|
+
Short flags for the option (e.g., ["-p", "-P"]).
|
|
161
|
+
long_flags (list[str], optional):
|
|
162
|
+
Long flags for the option (e.g., ["--port", "--p"]).
|
|
163
|
+
is_flag (bool):
|
|
164
|
+
Whether this is a boolean flag (no value needed).
|
|
165
|
+
Defaults to `False`.
|
|
166
|
+
type (Type[Any], optional):
|
|
167
|
+
The type to convert the value to (`int`, `str`, `float`, `bool`).
|
|
168
|
+
nargs (int):
|
|
169
|
+
Number of arguments each occurrence accepts. Defaults to `1`.
|
|
170
|
+
multiple (bool):
|
|
171
|
+
Whether the option can be provided multiple times.
|
|
172
|
+
Defaults to `False`.
|
|
173
|
+
help (str, optional):
|
|
174
|
+
Help text for this option.
|
|
175
|
+
required (bool):
|
|
176
|
+
Whether this option is required. Defaults to `False`.
|
|
177
|
+
default (Any):
|
|
178
|
+
Default value if not provided. Defaults to `None`.
|
|
179
|
+
tags (str | list[str], optional):
|
|
180
|
+
Tag(s) to associate with this option for grouping.
|
|
181
|
+
**kwargs (Any):
|
|
182
|
+
Additional keyword arguments passed to __init__ and load().
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Callable:
|
|
186
|
+
A decorator function that registers the option node.
|
|
187
|
+
"""
|
|
188
|
+
return super().as_decorator(
|
|
189
|
+
name=name,
|
|
190
|
+
param=param,
|
|
191
|
+
short_flags=short_flags,
|
|
192
|
+
long_flags=long_flags,
|
|
193
|
+
is_flag=is_flag,
|
|
194
|
+
type=type,
|
|
195
|
+
nargs=nargs,
|
|
196
|
+
multiple=multiple,
|
|
197
|
+
help=help,
|
|
198
|
+
required=required,
|
|
199
|
+
default=default,
|
|
200
|
+
tags=tags,
|
|
201
|
+
**kwargs,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
__all__ = ["OptionNode"]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""ParentNode class for parameter nodes (Option, Argument, Env)."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-instance-attributes
|
|
4
|
+
# pylint: disable=too-many-arguments
|
|
5
|
+
# pylint: disable=too-many-positional-arguments
|
|
6
|
+
# pylint: disable=redefined-builtin
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Callable, ParamSpec, TypeVar, cast
|
|
12
|
+
|
|
13
|
+
from click_extended.core.nodes.node import Node
|
|
14
|
+
from click_extended.core.other._tree import Tree
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from click_extended.core.nodes._root_node import RootNode
|
|
18
|
+
from click_extended.core.other.context import Context
|
|
19
|
+
|
|
20
|
+
P = ParamSpec("P")
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ParentNode(Node, ABC):
|
|
25
|
+
"""
|
|
26
|
+
Abstract base class for nodes that manage child nodes and inject values.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
parent: "RootNode"
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
name: str,
|
|
34
|
+
param: str | None = None,
|
|
35
|
+
help: str | None = None,
|
|
36
|
+
required: bool = False,
|
|
37
|
+
default: Any = None,
|
|
38
|
+
tags: str | list[str] | None = None,
|
|
39
|
+
**kwargs: Any,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Initialize a new `ParentNode` instance.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
name (str):
|
|
46
|
+
The name of the node (parameter name for injection).
|
|
47
|
+
param (str, optional):
|
|
48
|
+
The parameter name to inject into the function.
|
|
49
|
+
If not provided, uses name.
|
|
50
|
+
help (str, optional):
|
|
51
|
+
Help text for this parameter. If not provided,
|
|
52
|
+
may use function's docstring.
|
|
53
|
+
required (bool):
|
|
54
|
+
Whether this parameter is required. Defaults to False.
|
|
55
|
+
default (Any):
|
|
56
|
+
Default value if not provided. Defaults to `None`.
|
|
57
|
+
tags (str | list[str], optional):
|
|
58
|
+
Tag(s) to associate with this parameter for grouping.
|
|
59
|
+
Can be a single string or list of strings.
|
|
60
|
+
**kwargs (Any):
|
|
61
|
+
Additional keyword arguments (ignored in base class,
|
|
62
|
+
subclasses can use them if they override __init__).
|
|
63
|
+
"""
|
|
64
|
+
super().__init__(name=name, children={})
|
|
65
|
+
self.param = param if param is not None else name
|
|
66
|
+
self.help = help
|
|
67
|
+
self.required = required
|
|
68
|
+
self.default = default
|
|
69
|
+
|
|
70
|
+
if tags is None:
|
|
71
|
+
self.tags: list[str] = []
|
|
72
|
+
elif isinstance(tags, str):
|
|
73
|
+
self.tags = [tags]
|
|
74
|
+
else:
|
|
75
|
+
self.tags = list(tags)
|
|
76
|
+
|
|
77
|
+
self.was_provided: bool = False
|
|
78
|
+
self.raw_value: Any = None
|
|
79
|
+
self.cached_value: Any = None
|
|
80
|
+
self._value_computed: bool = False
|
|
81
|
+
self.decorator_kwargs: dict[str, Any] = {}
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def load(self, context: "Context", *args: Any, **kwargs: Any) -> Any:
|
|
85
|
+
"""
|
|
86
|
+
Load and return the value for this node.
|
|
87
|
+
|
|
88
|
+
This method must be implemented by all ParentNode subclasses to define
|
|
89
|
+
how values are obtained. The method can be either synchronous or
|
|
90
|
+
asynchronous.
|
|
91
|
+
|
|
92
|
+
For self-sourcing nodes (e.g., @env), this method retrieves the value
|
|
93
|
+
from its source (environment variables, config files, etc.).
|
|
94
|
+
|
|
95
|
+
For CLI-sourcing nodes (ArgumentNode, OptionNode), subclasses override
|
|
96
|
+
this signature to include a `value` parameter with the parsed CLI input.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
context (Context):
|
|
100
|
+
The current context instance containing node data and state.
|
|
101
|
+
*args (Any):
|
|
102
|
+
Optional positional arguments.
|
|
103
|
+
**kwargs (Any):
|
|
104
|
+
Optional keyword arguments.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Any:
|
|
108
|
+
The loaded value to inject into the function. Can return `None`
|
|
109
|
+
if that's a valid value for this node.
|
|
110
|
+
"""
|
|
111
|
+
raise NotImplementedError
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def as_decorator(
|
|
115
|
+
cls,
|
|
116
|
+
*,
|
|
117
|
+
name: str,
|
|
118
|
+
param: str | None = None,
|
|
119
|
+
help: str | None = None,
|
|
120
|
+
required: bool = False,
|
|
121
|
+
default: Any = None,
|
|
122
|
+
tags: str | list[str] | None = None,
|
|
123
|
+
**kwargs: Any,
|
|
124
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
125
|
+
"""
|
|
126
|
+
Return a decorator representation of the parent node.
|
|
127
|
+
|
|
128
|
+
All configuration parameters are stored and passed to the load() method.
|
|
129
|
+
Subclasses can override __init__ to accept additional parameters.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
name (str):
|
|
133
|
+
The name of the node (parameter name for injection).
|
|
134
|
+
param (str, optional):
|
|
135
|
+
The parameter name to inject into the function.
|
|
136
|
+
If not provided, uses name.
|
|
137
|
+
help (str, optional):
|
|
138
|
+
Help text for this parameter.
|
|
139
|
+
required (bool):
|
|
140
|
+
Whether this parameter is required. Defaults to False.
|
|
141
|
+
default (Any):
|
|
142
|
+
Default value if not provided. Defaults to `None`.
|
|
143
|
+
tags (str | list[str], optional):
|
|
144
|
+
Tag(s) to associate with this parameter for grouping.
|
|
145
|
+
**kwargs (Any):
|
|
146
|
+
Additional keyword arguments specific to the subclass.
|
|
147
|
+
These are passed to both __init__ and load().
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Callable:
|
|
151
|
+
A decorator function that registers the parent node.
|
|
152
|
+
"""
|
|
153
|
+
config = {
|
|
154
|
+
"name": name,
|
|
155
|
+
"param": param,
|
|
156
|
+
"help": help,
|
|
157
|
+
"required": required,
|
|
158
|
+
"default": default,
|
|
159
|
+
"tags": tags,
|
|
160
|
+
**kwargs,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
164
|
+
"""The actual decorator that wraps the function."""
|
|
165
|
+
instance = cls(**config) # type: ignore
|
|
166
|
+
instance.decorator_kwargs = config
|
|
167
|
+
Tree.queue_parent(instance)
|
|
168
|
+
|
|
169
|
+
if asyncio.iscoroutinefunction(func):
|
|
170
|
+
|
|
171
|
+
@wraps(func)
|
|
172
|
+
async def async_wrapper(
|
|
173
|
+
*call_args: P.args, **call_kwargs: P.kwargs
|
|
174
|
+
) -> T:
|
|
175
|
+
"""Async wrapper that preserves the original function."""
|
|
176
|
+
result = await func(*call_args, **call_kwargs)
|
|
177
|
+
return cast(T, result)
|
|
178
|
+
|
|
179
|
+
return cast(Callable[P, T], async_wrapper)
|
|
180
|
+
|
|
181
|
+
@wraps(func)
|
|
182
|
+
def wrapper(*call_args: P.args, **call_kwargs: P.kwargs) -> T:
|
|
183
|
+
"""Wrapper that preserves the original function."""
|
|
184
|
+
return func(*call_args, **call_kwargs)
|
|
185
|
+
|
|
186
|
+
return wrapper
|
|
187
|
+
|
|
188
|
+
return decorator
|
|
189
|
+
|
|
190
|
+
def get_value(self) -> Any:
|
|
191
|
+
"""
|
|
192
|
+
Get the cached value of the `ParentNode`.
|
|
193
|
+
|
|
194
|
+
Returns the cached value that was set after calling load() and
|
|
195
|
+
processing through any child nodes.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Any:
|
|
199
|
+
The cached processed value.
|
|
200
|
+
"""
|
|
201
|
+
return self.cached_value
|
|
202
|
+
|
|
203
|
+
def get_display_name(self) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Get a formatted display name for error messages.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
str:
|
|
209
|
+
The formatted name for display in error messages.
|
|
210
|
+
Base implementation returns the name as-is.
|
|
211
|
+
"""
|
|
212
|
+
return self.name
|
|
213
|
+
|
|
214
|
+
def __repr__(self) -> str:
|
|
215
|
+
"""Return a detailed representation of the parent node."""
|
|
216
|
+
class_name = self.__class__.__name__
|
|
217
|
+
return f"<{class_name} name='{self.name}'>"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
__all__ = ["ParentNode"]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""ValidationNode class for global validation logic."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, ParamSpec, TypeVar
|
|
6
|
+
|
|
7
|
+
from click_extended.core.nodes.node import Node
|
|
8
|
+
from click_extended.core.other._tree import Tree
|
|
9
|
+
from click_extended.utils.casing import Casing
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from click_extended.core.other.context import Context
|
|
13
|
+
from click_extended.types import Decorator
|
|
14
|
+
|
|
15
|
+
P = ParamSpec("P")
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ValidationNode(Node, ABC):
|
|
20
|
+
"""
|
|
21
|
+
Base class for validation nodes that run at specific lifecycle points.
|
|
22
|
+
|
|
23
|
+
Unlike ChildNodes which validate individual parent values, ValidationNodes
|
|
24
|
+
operate at the tree level and can access all parents through the context.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
name: str,
|
|
30
|
+
process_args: tuple[Any, ...] | None = None,
|
|
31
|
+
process_kwargs: dict[str, Any] | None = None,
|
|
32
|
+
**kwargs: Any,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Initialize a new `ValidationNode` instance.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
name (str):
|
|
39
|
+
The name of the validation node.
|
|
40
|
+
process_args (tuple):
|
|
41
|
+
Positional arguments to pass to lifecycle methods.
|
|
42
|
+
process_kwargs (dict[str, Any]):
|
|
43
|
+
Keyword arguments to pass to lifecycle methods.
|
|
44
|
+
**kwargs (Any):
|
|
45
|
+
Additional keyword arguments.
|
|
46
|
+
"""
|
|
47
|
+
children = kwargs.pop("children", None)
|
|
48
|
+
super().__init__(name=name, children=children, **kwargs)
|
|
49
|
+
self.process_args = process_args or ()
|
|
50
|
+
self.process_kwargs = process_kwargs or {}
|
|
51
|
+
|
|
52
|
+
def on_init(self, context: "Context", *args: Any, **kwargs: Any) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Run before any parent nodes are processed.
|
|
55
|
+
|
|
56
|
+
This hook is called after tree validation (Phase 3) but before
|
|
57
|
+
any parent node values are loaded.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
context (Context):
|
|
61
|
+
The current context with access to all nodes.
|
|
62
|
+
*args (Any):
|
|
63
|
+
Additional positional arguments from decorator.
|
|
64
|
+
**kwargs (Any):
|
|
65
|
+
Additional keyword arguments from decorator.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
Any exception to abort command execution.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def on_finalize(
|
|
72
|
+
self, context: "Context", *args: Any, **kwargs: Any
|
|
73
|
+
) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Run after all parent nodes are processed.
|
|
76
|
+
|
|
77
|
+
This hook is called after all parent and tag processing is complete,
|
|
78
|
+
but before the decorated function is called.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
context (Context):
|
|
82
|
+
The current context with access to all processed values.
|
|
83
|
+
*args (Any):
|
|
84
|
+
Additional positional arguments from decorator.
|
|
85
|
+
**kwargs (Any):
|
|
86
|
+
Additional keyword arguments from decorator.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
Any exception to abort command execution.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def as_decorator(cls, *args: Any, **kwargs: Any) -> "Decorator":
|
|
94
|
+
"""
|
|
95
|
+
Return a decorator representation of the validation node.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
*args (Any):
|
|
99
|
+
Positional arguments to pass to lifecycle methods.
|
|
100
|
+
**kwargs (Any):
|
|
101
|
+
Keyword arguments to pass to lifecycle methods.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Decorator:
|
|
105
|
+
A decorator function that registers the validation node.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
109
|
+
"""The actual decorator that wraps the function."""
|
|
110
|
+
name = Casing.to_snake_case(cls.__name__)
|
|
111
|
+
instance = cls(name=name, process_args=args, process_kwargs=kwargs)
|
|
112
|
+
Tree.queue_validation(instance)
|
|
113
|
+
|
|
114
|
+
@wraps(func)
|
|
115
|
+
def wrapper(*call_args: P.args, **call_kwargs: P.kwargs) -> T:
|
|
116
|
+
"""Wrapper that preserves the original function."""
|
|
117
|
+
return func(*call_args, **call_kwargs)
|
|
118
|
+
|
|
119
|
+
return wrapper
|
|
120
|
+
|
|
121
|
+
return decorator
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = ["ValidationNode"]
|