click-extended 0.4.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.
Files changed (149) hide show
  1. click_extended/__init__.py +10 -6
  2. click_extended/classes.py +12 -8
  3. click_extended/core/__init__.py +10 -0
  4. click_extended/core/decorators/__init__.py +21 -0
  5. click_extended/core/decorators/argument.py +227 -0
  6. click_extended/core/decorators/command.py +93 -0
  7. click_extended/core/decorators/env.py +155 -0
  8. click_extended/core/decorators/group.py +96 -0
  9. click_extended/core/decorators/option.py +347 -0
  10. click_extended/core/decorators/prompt.py +69 -0
  11. click_extended/core/decorators/selection.py +155 -0
  12. click_extended/core/decorators/tag.py +109 -0
  13. click_extended/core/nodes/__init__.py +21 -0
  14. click_extended/core/nodes/_root_node.py +1012 -0
  15. click_extended/core/nodes/argument_node.py +165 -0
  16. click_extended/core/nodes/child_node.py +555 -0
  17. click_extended/core/nodes/child_validation_node.py +100 -0
  18. click_extended/core/nodes/node.py +55 -0
  19. click_extended/core/nodes/option_node.py +205 -0
  20. click_extended/core/nodes/parent_node.py +220 -0
  21. click_extended/core/nodes/validation_node.py +124 -0
  22. click_extended/core/other/__init__.py +7 -0
  23. click_extended/core/other/_click_command.py +60 -0
  24. click_extended/core/other/_click_group.py +246 -0
  25. click_extended/core/other/_tree.py +491 -0
  26. click_extended/core/other/context.py +496 -0
  27. click_extended/decorators/__init__.py +29 -0
  28. click_extended/decorators/check/__init__.py +57 -0
  29. click_extended/decorators/check/conflicts.py +149 -0
  30. click_extended/decorators/check/contains.py +69 -0
  31. click_extended/decorators/check/dependencies.py +115 -0
  32. click_extended/decorators/check/divisible_by.py +48 -0
  33. click_extended/decorators/check/ends_with.py +85 -0
  34. click_extended/decorators/check/exclusive.py +75 -0
  35. click_extended/decorators/check/falsy.py +37 -0
  36. click_extended/decorators/check/is_email.py +43 -0
  37. click_extended/decorators/check/is_hex_color.py +41 -0
  38. click_extended/decorators/check/is_hostname.py +47 -0
  39. click_extended/decorators/check/is_ipv4.py +46 -0
  40. click_extended/decorators/check/is_ipv6.py +46 -0
  41. click_extended/decorators/check/is_json.py +40 -0
  42. click_extended/decorators/check/is_mac_address.py +40 -0
  43. click_extended/decorators/check/is_negative.py +37 -0
  44. click_extended/decorators/check/is_non_zero.py +37 -0
  45. click_extended/decorators/check/is_port.py +39 -0
  46. click_extended/decorators/check/is_positive.py +37 -0
  47. click_extended/decorators/check/is_url.py +75 -0
  48. click_extended/decorators/check/is_uuid.py +40 -0
  49. click_extended/decorators/check/length.py +68 -0
  50. click_extended/decorators/check/not_empty.py +49 -0
  51. click_extended/decorators/check/regex.py +47 -0
  52. click_extended/decorators/check/requires.py +190 -0
  53. click_extended/decorators/check/starts_with.py +87 -0
  54. click_extended/decorators/check/truthy.py +37 -0
  55. click_extended/decorators/compare/__init__.py +15 -0
  56. click_extended/decorators/compare/at_least.py +57 -0
  57. click_extended/decorators/compare/at_most.py +57 -0
  58. click_extended/decorators/compare/between.py +119 -0
  59. click_extended/decorators/compare/greater_than.py +183 -0
  60. click_extended/decorators/compare/less_than.py +183 -0
  61. click_extended/decorators/convert/__init__.py +31 -0
  62. click_extended/decorators/convert/convert_angle.py +94 -0
  63. click_extended/decorators/convert/convert_area.py +123 -0
  64. click_extended/decorators/convert/convert_bits.py +211 -0
  65. click_extended/decorators/convert/convert_distance.py +154 -0
  66. click_extended/decorators/convert/convert_energy.py +155 -0
  67. click_extended/decorators/convert/convert_power.py +128 -0
  68. click_extended/decorators/convert/convert_pressure.py +131 -0
  69. click_extended/decorators/convert/convert_speed.py +122 -0
  70. click_extended/decorators/convert/convert_temperature.py +89 -0
  71. click_extended/decorators/convert/convert_time.py +108 -0
  72. click_extended/decorators/convert/convert_volume.py +218 -0
  73. click_extended/decorators/convert/convert_weight.py +158 -0
  74. click_extended/decorators/load/__init__.py +13 -0
  75. click_extended/decorators/load/load_csv.py +117 -0
  76. click_extended/decorators/load/load_json.py +61 -0
  77. click_extended/decorators/load/load_toml.py +47 -0
  78. click_extended/decorators/load/load_yaml.py +72 -0
  79. click_extended/decorators/math/__init__.py +37 -0
  80. click_extended/decorators/math/absolute.py +35 -0
  81. click_extended/decorators/math/add.py +48 -0
  82. click_extended/decorators/math/ceil.py +36 -0
  83. click_extended/decorators/math/clamp.py +51 -0
  84. click_extended/decorators/math/divide.py +42 -0
  85. click_extended/decorators/math/floor.py +36 -0
  86. click_extended/decorators/math/maximum.py +39 -0
  87. click_extended/decorators/math/minimum.py +39 -0
  88. click_extended/decorators/math/modulo.py +39 -0
  89. click_extended/decorators/math/multiply.py +51 -0
  90. click_extended/decorators/math/normalize.py +76 -0
  91. click_extended/decorators/math/power.py +39 -0
  92. click_extended/decorators/math/rounded.py +39 -0
  93. click_extended/decorators/math/sqrt.py +39 -0
  94. click_extended/decorators/math/subtract.py +39 -0
  95. click_extended/decorators/math/to_percent.py +63 -0
  96. click_extended/decorators/misc/__init__.py +17 -0
  97. click_extended/decorators/misc/choice.py +139 -0
  98. click_extended/decorators/misc/confirm_if.py +147 -0
  99. click_extended/decorators/misc/default.py +95 -0
  100. click_extended/decorators/misc/deprecated.py +131 -0
  101. click_extended/decorators/misc/experimental.py +79 -0
  102. click_extended/decorators/misc/now.py +42 -0
  103. click_extended/decorators/random/__init__.py +21 -0
  104. click_extended/decorators/random/random_bool.py +49 -0
  105. click_extended/decorators/random/random_choice.py +63 -0
  106. click_extended/decorators/random/random_datetime.py +140 -0
  107. click_extended/decorators/random/random_float.py +62 -0
  108. click_extended/decorators/random/random_integer.py +56 -0
  109. click_extended/decorators/random/random_prime.py +196 -0
  110. click_extended/decorators/random/random_string.py +77 -0
  111. click_extended/decorators/random/random_uuid.py +119 -0
  112. click_extended/decorators/transform/__init__.py +71 -0
  113. click_extended/decorators/transform/add_prefix.py +58 -0
  114. click_extended/decorators/transform/add_suffix.py +58 -0
  115. click_extended/decorators/transform/apply.py +35 -0
  116. click_extended/decorators/transform/basename.py +44 -0
  117. click_extended/decorators/transform/dirname.py +44 -0
  118. click_extended/decorators/transform/expand_vars.py +36 -0
  119. click_extended/decorators/transform/remove_prefix.py +57 -0
  120. click_extended/decorators/transform/remove_suffix.py +57 -0
  121. click_extended/decorators/transform/replace.py +46 -0
  122. click_extended/decorators/transform/slugify.py +45 -0
  123. click_extended/decorators/transform/split.py +43 -0
  124. click_extended/decorators/transform/strip.py +148 -0
  125. click_extended/decorators/transform/to_case.py +216 -0
  126. click_extended/decorators/transform/to_date.py +75 -0
  127. click_extended/decorators/transform/to_datetime.py +83 -0
  128. click_extended/decorators/transform/to_path.py +274 -0
  129. click_extended/decorators/transform/to_time.py +77 -0
  130. click_extended/decorators/transform/to_timestamp.py +114 -0
  131. click_extended/decorators/transform/truncate.py +47 -0
  132. click_extended/types.py +1 -1
  133. click_extended/utils/__init__.py +13 -0
  134. click_extended/utils/casing.py +169 -0
  135. click_extended/utils/checks.py +48 -0
  136. click_extended/utils/dispatch.py +1016 -0
  137. click_extended/utils/format.py +101 -0
  138. click_extended/utils/humanize.py +209 -0
  139. click_extended/utils/naming.py +238 -0
  140. click_extended/utils/process.py +294 -0
  141. click_extended/utils/selection.py +267 -0
  142. click_extended/utils/time.py +46 -0
  143. {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/METADATA +100 -29
  144. click_extended-1.0.1.dist-info/RECORD +149 -0
  145. click_extended-0.4.0.dist-info/RECORD +0 -10
  146. {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/WHEEL +0 -0
  147. {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/licenses/AUTHORS.md +0 -0
  148. {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/licenses/LICENSE +0 -0
  149. {click_extended-0.4.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"]