click-extended 1.0.0__py3-none-any.whl → 1.0.2__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/__init__.py +2 -0
- 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/context.py +56 -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.2.dist-info}/METADATA +2 -1
- click_extended-1.0.2.dist-info/RECORD +150 -0
- click_extended-1.0.0.dist-info/RECORD +0 -10
- {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/WHEEL +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/licenses/AUTHORS.md +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class for storing the nodes of the current context.
|
|
3
|
+
|
|
4
|
+
Phases:
|
|
5
|
+
- **Phase 1 (Registration)**:
|
|
6
|
+
Nodes are queued during decorator application.
|
|
7
|
+
- **Phase 2 (Initialization)**:
|
|
8
|
+
Click context is created and metadata is injected.
|
|
9
|
+
- **Phase 3 (Validation)**:
|
|
10
|
+
Tree is built and validated with full context.
|
|
11
|
+
- **Phase 4 (Runtime)**:
|
|
12
|
+
Parameters are processed with scope tracking.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# pylint: disable=import-outside-toplevel
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
|
|
23
|
+
from click_extended.errors import (
|
|
24
|
+
NameExistsError,
|
|
25
|
+
NoParentError,
|
|
26
|
+
NoRootError,
|
|
27
|
+
ParentExistsError,
|
|
28
|
+
RootExistsError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from click_extended.core.decorators.tag import Tag
|
|
33
|
+
from click_extended.core.nodes._root_node import RootNode
|
|
34
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
35
|
+
from click_extended.core.nodes.child_validation_node import (
|
|
36
|
+
ChildValidationNode,
|
|
37
|
+
)
|
|
38
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
39
|
+
from click_extended.core.nodes.validation_node import ValidationNode
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Tree:
|
|
43
|
+
"""
|
|
44
|
+
Class for managing the node tree and lifecycle phases.
|
|
45
|
+
|
|
46
|
+
The tree coordinates all four lifecycle phases:
|
|
47
|
+
|
|
48
|
+
- **Phase 1 (Registration)**:
|
|
49
|
+
Nodes are queued during decorator application.
|
|
50
|
+
- **Phase 2 (Initialization)**:
|
|
51
|
+
Click context is created and metadata is injected.
|
|
52
|
+
- **Phase 3 (Validation)**:
|
|
53
|
+
Tree is built and validated with full context.
|
|
54
|
+
- **Phase 4 (Runtime)**:
|
|
55
|
+
Parameters are processed with scope tracking.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
root (RootNode | None):
|
|
59
|
+
The root node of the tree
|
|
60
|
+
recent (ParentNode | None):
|
|
61
|
+
Most recently registered parent node
|
|
62
|
+
recent_tag (Tag | None):
|
|
63
|
+
Most recently registered tag
|
|
64
|
+
tags (dict[str, Tag]):
|
|
65
|
+
Dictionary of all tags
|
|
66
|
+
globals (list[GlobalNode]):
|
|
67
|
+
List of global nodes
|
|
68
|
+
data (dict[str, Any]):
|
|
69
|
+
Custom data storage
|
|
70
|
+
is_validated (bool):
|
|
71
|
+
Whether Phase 3 validation has completed.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
_pending_nodes: list[
|
|
75
|
+
tuple[
|
|
76
|
+
Literal[
|
|
77
|
+
"parent",
|
|
78
|
+
"child",
|
|
79
|
+
"tag",
|
|
80
|
+
"validation",
|
|
81
|
+
"child_validation",
|
|
82
|
+
],
|
|
83
|
+
"ParentNode | ChildNode | Tag | ValidationNode | ChildValidationNode", # pylint: disable=line-too-long
|
|
84
|
+
]
|
|
85
|
+
] = []
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def get_pending_nodes() -> list[
|
|
89
|
+
tuple[
|
|
90
|
+
Literal[
|
|
91
|
+
"parent",
|
|
92
|
+
"child",
|
|
93
|
+
"tag",
|
|
94
|
+
"validation",
|
|
95
|
+
"child_validation",
|
|
96
|
+
],
|
|
97
|
+
"ParentNode | ChildNode | Tag | ValidationNode | ChildValidationNode", # pylint: disable=line-too-long
|
|
98
|
+
]
|
|
99
|
+
]:
|
|
100
|
+
"""
|
|
101
|
+
Get and clear the pending nodes queue.
|
|
102
|
+
This is where decorators queue nodes during bottom-to-top application.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
list:
|
|
106
|
+
List of queued nodes with their types.
|
|
107
|
+
"""
|
|
108
|
+
nodes = Tree._pending_nodes.copy()
|
|
109
|
+
Tree._pending_nodes.clear()
|
|
110
|
+
return nodes
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def queue_parent(node: "ParentNode") -> None:
|
|
114
|
+
"""
|
|
115
|
+
Queue a parent node for registration.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
node (ParentNode):
|
|
119
|
+
The parent node to queue.
|
|
120
|
+
"""
|
|
121
|
+
Tree._pending_nodes.append(("parent", node))
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def queue_child(node: "ChildNode") -> None:
|
|
125
|
+
"""
|
|
126
|
+
Queue a child node for registration.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
node (ChildNode):
|
|
130
|
+
The child node to queue.
|
|
131
|
+
"""
|
|
132
|
+
Tree._pending_nodes.append(("child", node))
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def queue_tag(node: "Tag") -> None:
|
|
136
|
+
"""
|
|
137
|
+
Queue a tag node for registration.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
node (Tag):
|
|
141
|
+
The tag to queue.
|
|
142
|
+
"""
|
|
143
|
+
Tree._pending_nodes.append(("tag", node))
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def queue_validation(node: "ValidationNode") -> None:
|
|
147
|
+
"""
|
|
148
|
+
Queue a validation node for registration.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
node (ValidationNode):
|
|
152
|
+
The validation node to queue.
|
|
153
|
+
"""
|
|
154
|
+
Tree._pending_nodes.append(("validation", node))
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def queue_child_validation(node: "ChildValidationNode") -> None:
|
|
158
|
+
"""
|
|
159
|
+
Queue a child validation node for registration.
|
|
160
|
+
|
|
161
|
+
Child validation nodes can act as both child nodes and
|
|
162
|
+
validation nodes. The registration phase determines which
|
|
163
|
+
behavior to use based on decorator placement.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
node (ChildValidationNode):
|
|
167
|
+
The child validation node to queue.
|
|
168
|
+
"""
|
|
169
|
+
Tree._pending_nodes.append(("child_validation", node))
|
|
170
|
+
|
|
171
|
+
def __init__(self) -> None:
|
|
172
|
+
"""Initialize a new Tree instance."""
|
|
173
|
+
self.root: "RootNode | None" = None
|
|
174
|
+
self.recent: "ParentNode | None" = None
|
|
175
|
+
self.recent_tag: "Tag | None" = None
|
|
176
|
+
self.tags: dict[str, "Tag"] = {}
|
|
177
|
+
self.validations: list["ValidationNode"] = []
|
|
178
|
+
self.data: dict[str, Any] = {}
|
|
179
|
+
self.is_validated: bool = False
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def initialize_context(
|
|
183
|
+
context: click.Context, root_node: "RootNode"
|
|
184
|
+
) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Initialize Click context with `click-extended` metadata.
|
|
187
|
+
This is a part of `phase 2` and must be called before any
|
|
188
|
+
validation or processing occurs.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
context (click.Context):
|
|
192
|
+
The Click context to initialize.
|
|
193
|
+
root_node (RootNode):
|
|
194
|
+
The root node of the tree.
|
|
195
|
+
"""
|
|
196
|
+
parents_dict: dict[str, "ParentNode"] = {}
|
|
197
|
+
if (root := root_node.tree.root) is not None:
|
|
198
|
+
parents_dict = {
|
|
199
|
+
name: node # type: ignore[misc]
|
|
200
|
+
for name, node in root.children.items()
|
|
201
|
+
if isinstance(name, str)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
children_dict: dict[str, "ChildNode"] = {}
|
|
205
|
+
for parent in parents_dict.values():
|
|
206
|
+
for child_name, child_node in parent.children.items():
|
|
207
|
+
if isinstance(child_name, (str, int)):
|
|
208
|
+
name = child_node.name
|
|
209
|
+
children_dict[name] = child_node # type: ignore
|
|
210
|
+
|
|
211
|
+
for tag in root_node.tree.tags.values():
|
|
212
|
+
for child_name, child_node in tag.children.items():
|
|
213
|
+
if isinstance(child_name, (str, int)):
|
|
214
|
+
name = child_node.name
|
|
215
|
+
children_dict[name] = child_node # type: ignore
|
|
216
|
+
|
|
217
|
+
debug = os.getenv("CLICK_EXTENDED_DEBUG", "").lower() in (
|
|
218
|
+
"1",
|
|
219
|
+
"true",
|
|
220
|
+
"yes",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
context.meta["click_extended"] = {
|
|
224
|
+
"current_scope": "root",
|
|
225
|
+
"root_node": root_node,
|
|
226
|
+
"parent_node": None,
|
|
227
|
+
"child_node": None,
|
|
228
|
+
"parents": parents_dict,
|
|
229
|
+
"tags": root_node.tree.tags,
|
|
230
|
+
"children": children_dict,
|
|
231
|
+
"data": {},
|
|
232
|
+
"debug": debug,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def update_scope(
|
|
237
|
+
context: click.Context,
|
|
238
|
+
scope: Literal["root", "parent", "child"],
|
|
239
|
+
parent_node: "ParentNode | None" = None,
|
|
240
|
+
child_node: "ChildNode | None" = None,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Update the current scope in the context.
|
|
244
|
+
This is a part of `phase 4` and is called as the tree is traversed
|
|
245
|
+
during parameter processing.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
context (click.Context):
|
|
249
|
+
The Click context to update.
|
|
250
|
+
scope (str):
|
|
251
|
+
The new scope level, must either be `root`, `parent` or `child`.
|
|
252
|
+
parent_node (ParentNode | None, optional):
|
|
253
|
+
The current parent node (if in parent/child scope).
|
|
254
|
+
child_node (ChildNode | None, optional):
|
|
255
|
+
The current child node (if in child scope).
|
|
256
|
+
"""
|
|
257
|
+
if "click_extended" not in context.meta:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
context.meta["click_extended"]["current_scope"] = scope
|
|
261
|
+
context.meta["click_extended"]["parent_node"] = parent_node
|
|
262
|
+
context.meta["click_extended"]["child_node"] = child_node
|
|
263
|
+
|
|
264
|
+
def validate_and_build(self, context: click.Context) -> None:
|
|
265
|
+
"""
|
|
266
|
+
Build and validate the tree structure. This method is a part of
|
|
267
|
+
`phase 3` and is where all structural validation occurs which is
|
|
268
|
+
after the Click context has been initialized. All errors raised
|
|
269
|
+
here are `ContextAwareError` subclasses.
|
|
270
|
+
|
|
271
|
+
This method:
|
|
272
|
+
|
|
273
|
+
1. Builds the tree from pending nodes
|
|
274
|
+
2. Validates structure (root exists, parents/children linked)
|
|
275
|
+
3. Validates names (no collisions)
|
|
276
|
+
4. Validates types (child/parent compatibility)
|
|
277
|
+
5. Sets up tags and globals
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
context (click.Context):
|
|
281
|
+
The Click context (must be initialized).
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
RootExistsError:
|
|
285
|
+
If root already exists.
|
|
286
|
+
NoRootError:
|
|
287
|
+
If no root is defined.
|
|
288
|
+
ParentExistsError:
|
|
289
|
+
If duplicate parent names.
|
|
290
|
+
NoParentError:
|
|
291
|
+
If child has no parent.
|
|
292
|
+
NameExistsError:
|
|
293
|
+
If name collisions detected.
|
|
294
|
+
TypeMismatchError:
|
|
295
|
+
If child/parent types incompatible.
|
|
296
|
+
"""
|
|
297
|
+
if self.is_validated:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
if not self.root or not self.root.children:
|
|
301
|
+
pending = list(reversed(Tree.get_pending_nodes()))
|
|
302
|
+
|
|
303
|
+
for node_type, node_inst in pending:
|
|
304
|
+
if node_type == "parent":
|
|
305
|
+
self._register_parent_node(cast("ParentNode", node_inst))
|
|
306
|
+
elif node_type == "child":
|
|
307
|
+
self._register_child_node(cast("ChildNode", node_inst))
|
|
308
|
+
elif node_type == "tag":
|
|
309
|
+
self._register_tag_node(cast("Tag", node_inst))
|
|
310
|
+
elif node_type == "validation":
|
|
311
|
+
self._register_validation_node(
|
|
312
|
+
cast("ValidationNode", node_inst)
|
|
313
|
+
)
|
|
314
|
+
elif node_type == "child_validation":
|
|
315
|
+
self._register_child_validation_node(
|
|
316
|
+
cast("ChildValidationNode", node_inst)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
self._validate_names()
|
|
320
|
+
self.is_validated = True
|
|
321
|
+
|
|
322
|
+
def _register_parent_node(self, node: "ParentNode") -> None:
|
|
323
|
+
"""Register a parent node during validation phase."""
|
|
324
|
+
if self.root is None:
|
|
325
|
+
raise NoRootError()
|
|
326
|
+
|
|
327
|
+
if self.root.children.get(node.name) is not None:
|
|
328
|
+
raise ParentExistsError(node.name)
|
|
329
|
+
|
|
330
|
+
self.recent = node
|
|
331
|
+
self.root[node.name] = node
|
|
332
|
+
|
|
333
|
+
def _register_child_node(self, node: "ChildNode") -> None:
|
|
334
|
+
"""Register a child node during validation phase."""
|
|
335
|
+
if self.root is None:
|
|
336
|
+
raise NoRootError()
|
|
337
|
+
|
|
338
|
+
# Attach tag
|
|
339
|
+
if self.recent_tag is not None:
|
|
340
|
+
tag = self.recent_tag
|
|
341
|
+
|
|
342
|
+
if not self.has_handle_tag_implemented(node):
|
|
343
|
+
print(
|
|
344
|
+
f"Error ({tag.name}): Child node '{node.name}' can not be "
|
|
345
|
+
"used on a tag node.\nTip: Children attached to @tag "
|
|
346
|
+
"decorators must implement the handle_tag() method."
|
|
347
|
+
)
|
|
348
|
+
sys.exit(2)
|
|
349
|
+
|
|
350
|
+
index = len(tag)
|
|
351
|
+
tag[index] = node
|
|
352
|
+
|
|
353
|
+
# Attach parent
|
|
354
|
+
elif self.recent is not None:
|
|
355
|
+
parent_node = cast("ParentNode", self.root[self.recent.name])
|
|
356
|
+
index = len(parent_node)
|
|
357
|
+
parent_node[index] = node
|
|
358
|
+
|
|
359
|
+
else:
|
|
360
|
+
raise NoParentError(node.name)
|
|
361
|
+
|
|
362
|
+
def _register_tag_node(self, node: "Tag") -> None:
|
|
363
|
+
"""Register a tag node during validation phase."""
|
|
364
|
+
self.tags[node.name] = node
|
|
365
|
+
self.recent_tag = node
|
|
366
|
+
|
|
367
|
+
def _register_validation_node(self, node: "ValidationNode") -> None:
|
|
368
|
+
"""Register a validation node during validation phase."""
|
|
369
|
+
self.validations.append(node)
|
|
370
|
+
|
|
371
|
+
def _register_child_validation_node(
|
|
372
|
+
self, node: "ChildValidationNode"
|
|
373
|
+
) -> None:
|
|
374
|
+
"""
|
|
375
|
+
Register a child validation node during validation phase.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
node (ChildValidationNode):
|
|
379
|
+
The child validation node to register.
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
NoParentError:
|
|
383
|
+
If registering as child but no parent exists.
|
|
384
|
+
Provides enhanced message about child validation node
|
|
385
|
+
behavior.
|
|
386
|
+
"""
|
|
387
|
+
if self.recent is not None or self.recent_tag is not None:
|
|
388
|
+
try:
|
|
389
|
+
self._register_child_node(node)
|
|
390
|
+
except NoParentError as e:
|
|
391
|
+
raise NoParentError(
|
|
392
|
+
node.name,
|
|
393
|
+
tip=(
|
|
394
|
+
f"Child validation node '{node.name}' attempted "
|
|
395
|
+
"to attach as a child node but no parent was "
|
|
396
|
+
"found. Ensure a parent node "
|
|
397
|
+
"or tag is defined before the child "
|
|
398
|
+
"validation node decorator."
|
|
399
|
+
),
|
|
400
|
+
) from e
|
|
401
|
+
else:
|
|
402
|
+
self._register_validation_node(node)
|
|
403
|
+
|
|
404
|
+
def has_handle_tag_implemented(self, node: "ChildNode") -> bool:
|
|
405
|
+
"""Check if a child node has `handle_tag` implemented."""
|
|
406
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
407
|
+
|
|
408
|
+
handle_tag_method = getattr(type(node), "handle_tag", None)
|
|
409
|
+
|
|
410
|
+
if handle_tag_method is None:
|
|
411
|
+
return False
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
base_method = getattr(ChildNode, "handle_tag", None)
|
|
415
|
+
|
|
416
|
+
if handle_tag_method is base_method:
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
return True
|
|
420
|
+
except AttributeError:
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
def _validate_names(self) -> None:
|
|
424
|
+
"""
|
|
425
|
+
Validate that all names are unique.
|
|
426
|
+
|
|
427
|
+
Checks for collisions between options, arguments, envs, tags,
|
|
428
|
+
and globals. Also checks that parent nodes don't use their own
|
|
429
|
+
name as a tag.
|
|
430
|
+
|
|
431
|
+
Raises:
|
|
432
|
+
NameExistsError: If duplicate names found.
|
|
433
|
+
"""
|
|
434
|
+
if self.root is None:
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
seen_names: set[str] = set()
|
|
438
|
+
|
|
439
|
+
# Check parents
|
|
440
|
+
for parent_node in self.root.children.values():
|
|
441
|
+
if parent_node.name in seen_names:
|
|
442
|
+
raise NameExistsError(parent_node.name)
|
|
443
|
+
seen_names.add(parent_node.name)
|
|
444
|
+
|
|
445
|
+
if parent_node.name in cast("ParentNode", parent_node).tags:
|
|
446
|
+
raise NameExistsError(
|
|
447
|
+
parent_node.name,
|
|
448
|
+
tip=f"Parameter '{parent_node.name}' cannot use its own "
|
|
449
|
+
"name as a tag. Rename either the parameter or the tag "
|
|
450
|
+
"to avoid the conflict.",
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Check tags
|
|
454
|
+
for tag_name in self.tags:
|
|
455
|
+
if tag_name in seen_names:
|
|
456
|
+
raise NameExistsError(tag_name)
|
|
457
|
+
seen_names.add(tag_name)
|
|
458
|
+
|
|
459
|
+
def register_root(self, node: "RootNode") -> None:
|
|
460
|
+
"""
|
|
461
|
+
Register the root node. This is called in `phase 1`.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
node (RootNode):
|
|
465
|
+
The root node to register.
|
|
466
|
+
|
|
467
|
+
Raises:
|
|
468
|
+
RootExistsError:
|
|
469
|
+
If root already exists.
|
|
470
|
+
"""
|
|
471
|
+
if self.root is not None:
|
|
472
|
+
raise RootExistsError()
|
|
473
|
+
|
|
474
|
+
self.root = node
|
|
475
|
+
|
|
476
|
+
def visualize(self) -> None:
|
|
477
|
+
"""Visualize the tree structure."""
|
|
478
|
+
if self.root is None:
|
|
479
|
+
raise NoRootError()
|
|
480
|
+
|
|
481
|
+
print(self.root.name)
|
|
482
|
+
assert self.root.children is not None
|
|
483
|
+
for parent in self.root.children.values():
|
|
484
|
+
print(f" {parent.name}")
|
|
485
|
+
assert parent.children is not None
|
|
486
|
+
for child in parent.children.values():
|
|
487
|
+
print(f" {child.name}")
|
|
488
|
+
|
|
489
|
+
if self.validations:
|
|
490
|
+
for validation in self.validations:
|
|
491
|
+
print(f" {validation.name}")
|