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,1012 @@
|
|
|
1
|
+
"""The node used as a root node."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-locals
|
|
4
|
+
# pylint: disable=too-many-branches
|
|
5
|
+
# pylint: disable=too-many-statements
|
|
6
|
+
# pylint: disable=too-many-nested-blocks
|
|
7
|
+
# pylint: disable=too-many-lines
|
|
8
|
+
# pylint: disable=broad-exception-caught
|
|
9
|
+
# pylint: disable=protected-access
|
|
10
|
+
# pylint: disable=invalid-name
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import sys
|
|
14
|
+
import traceback
|
|
15
|
+
from functools import wraps
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast, get_type_hints
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
from click.utils import echo
|
|
20
|
+
|
|
21
|
+
from click_extended.core.decorators.env import Env
|
|
22
|
+
from click_extended.core.decorators.tag import Tag
|
|
23
|
+
from click_extended.core.nodes.argument_node import ArgumentNode
|
|
24
|
+
from click_extended.core.nodes.child_validation_node import ChildValidationNode
|
|
25
|
+
from click_extended.core.nodes.node import Node
|
|
26
|
+
from click_extended.core.nodes.option_node import OptionNode
|
|
27
|
+
from click_extended.core.other._tree import Tree
|
|
28
|
+
from click_extended.core.other.context import Context
|
|
29
|
+
from click_extended.errors import (
|
|
30
|
+
ContextAwareError,
|
|
31
|
+
NameExistsError,
|
|
32
|
+
NoRootError,
|
|
33
|
+
ProcessError,
|
|
34
|
+
UnhandledTypeError,
|
|
35
|
+
)
|
|
36
|
+
from click_extended.utils.humanize import humanize_type
|
|
37
|
+
from click_extended.utils.process import (
|
|
38
|
+
check_has_async_handlers,
|
|
39
|
+
process_children,
|
|
40
|
+
process_children_async,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
45
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
46
|
+
from click_extended.core.other._click_command import ClickCommand
|
|
47
|
+
from click_extended.core.other._click_group import ClickGroup
|
|
48
|
+
|
|
49
|
+
ClickType = TypeVar("ClickType", bound=click.Command)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RootNode(Node):
|
|
53
|
+
"""The node used as a root node for initializing a new context."""
|
|
54
|
+
|
|
55
|
+
parent: None
|
|
56
|
+
tree: Tree
|
|
57
|
+
aliases: str | list[str] | None
|
|
58
|
+
|
|
59
|
+
def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Initialize a new `RootNode` instance.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
name (str):
|
|
65
|
+
The name of the node.
|
|
66
|
+
*args (Any):
|
|
67
|
+
Additional positional arguments (stored but not passed to Node).
|
|
68
|
+
**kwargs (Any):
|
|
69
|
+
Additional keyword arguments (stored but not passed to Node).
|
|
70
|
+
May include 'aliases' for command/group aliases.
|
|
71
|
+
"""
|
|
72
|
+
super().__init__(name=name, children={})
|
|
73
|
+
self.aliases = kwargs.pop("aliases", None)
|
|
74
|
+
self.tree = Tree()
|
|
75
|
+
self.extra_args = args
|
|
76
|
+
self.extra_kwargs = kwargs
|
|
77
|
+
|
|
78
|
+
def format_name_with_aliases(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Format the node name with its aliases for display.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
str:
|
|
84
|
+
Formatted name like "name (alias1, alias2)"
|
|
85
|
+
"""
|
|
86
|
+
if not self.aliases:
|
|
87
|
+
return self.name
|
|
88
|
+
|
|
89
|
+
aliases_list = (
|
|
90
|
+
[self.aliases] if isinstance(self.aliases, str) else self.aliases
|
|
91
|
+
)
|
|
92
|
+
valid_aliases = [a for a in aliases_list if a]
|
|
93
|
+
|
|
94
|
+
if valid_aliases:
|
|
95
|
+
return f"{self.name} ({', '.join(valid_aliases)})"
|
|
96
|
+
|
|
97
|
+
return self.name
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def _get_click_decorator(cls) -> Callable[..., Any]:
|
|
101
|
+
"""
|
|
102
|
+
Return the Click decorator (command or group) to use.
|
|
103
|
+
|
|
104
|
+
Subclasses must override this to specify which Click decorator to use.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Callable:
|
|
108
|
+
The Click decorator function
|
|
109
|
+
(e.g.,`click.command`, `click.group`).
|
|
110
|
+
"""
|
|
111
|
+
raise NotImplementedError(
|
|
112
|
+
"Subclasses must implement _get_click_decorator()"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def _get_click_cls(cls) -> type["ClickCommand | ClickGroup"]:
|
|
117
|
+
"""
|
|
118
|
+
Return the Click class to use for this root node.
|
|
119
|
+
|
|
120
|
+
Subclasses must override this to specify which Click class to use.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
type[ClickCommand|ClickGroup]:
|
|
124
|
+
The Click class (e.g., `ClickCommand`, `ClickGroup`).
|
|
125
|
+
"""
|
|
126
|
+
raise NotImplementedError("Subclasses must implement _get_click_cls()")
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def _build_click_params(
|
|
130
|
+
cls,
|
|
131
|
+
func: Callable[..., Any],
|
|
132
|
+
instance: "RootNode",
|
|
133
|
+
) -> tuple[Callable[..., Any], bool]:
|
|
134
|
+
"""
|
|
135
|
+
Build Click decorators for options and arguments.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
tuple:
|
|
139
|
+
A tuple with `wrapped_func` and `h_flag_taken`
|
|
140
|
+
"""
|
|
141
|
+
h_flag_taken = False
|
|
142
|
+
seen_short_flags: dict[str, str] = {}
|
|
143
|
+
|
|
144
|
+
if not instance.tree.root or not instance.tree.root.children:
|
|
145
|
+
return func, h_flag_taken
|
|
146
|
+
|
|
147
|
+
for parent_node in instance.tree.root.children.values():
|
|
148
|
+
if isinstance(parent_node, OptionNode):
|
|
149
|
+
for short_flag in parent_node.short_flags:
|
|
150
|
+
if short_flag == "-h":
|
|
151
|
+
h_flag_taken = True
|
|
152
|
+
if short_flag in seen_short_flags:
|
|
153
|
+
prev_name = seen_short_flags[short_flag]
|
|
154
|
+
raise NameExistsError(
|
|
155
|
+
short_flag,
|
|
156
|
+
tip=f"Short flag '{short_flag}' is used by both "
|
|
157
|
+
f"'{prev_name}' and '{parent_node.name}'",
|
|
158
|
+
)
|
|
159
|
+
seen_short_flags[short_flag] = parent_node.name
|
|
160
|
+
|
|
161
|
+
parent_items = list(instance.tree.root.children.items())
|
|
162
|
+
|
|
163
|
+
option_nodes = [
|
|
164
|
+
(name, node)
|
|
165
|
+
for name, node in parent_items
|
|
166
|
+
if isinstance(node, OptionNode)
|
|
167
|
+
]
|
|
168
|
+
argument_nodes = [
|
|
169
|
+
(name, node)
|
|
170
|
+
for name, node in parent_items
|
|
171
|
+
if isinstance(node, ArgumentNode)
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
for _parent_name, parent_node in option_nodes:
|
|
175
|
+
params: list[str] = []
|
|
176
|
+
params.extend(parent_node.short_flags)
|
|
177
|
+
params.extend(parent_node.long_flags)
|
|
178
|
+
params.append(parent_node.name)
|
|
179
|
+
|
|
180
|
+
option_kwargs: dict[str, Any] = {
|
|
181
|
+
"type": parent_node.type,
|
|
182
|
+
"required": parent_node.required,
|
|
183
|
+
"is_flag": parent_node.is_flag,
|
|
184
|
+
"help": parent_node.help,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
extra_kwargs = getattr(parent_node, "extra_kwargs", {})
|
|
188
|
+
if extra_kwargs:
|
|
189
|
+
option_kwargs.update(extra_kwargs)
|
|
190
|
+
|
|
191
|
+
if not parent_node.required or parent_node.default is not None:
|
|
192
|
+
option_kwargs["default"] = parent_node.default
|
|
193
|
+
|
|
194
|
+
if parent_node.multiple:
|
|
195
|
+
option_kwargs["multiple"] = True
|
|
196
|
+
if parent_node.nargs > 1:
|
|
197
|
+
option_kwargs["nargs"] = parent_node.nargs
|
|
198
|
+
|
|
199
|
+
func = click.option(*params, **option_kwargs)(func)
|
|
200
|
+
|
|
201
|
+
for _parent_name, parent_node in argument_nodes:
|
|
202
|
+
arg_kwargs: dict[str, Any] = {
|
|
203
|
+
"type": parent_node.type,
|
|
204
|
+
"required": parent_node.required,
|
|
205
|
+
"nargs": parent_node.nargs,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
extra_kwargs = getattr(parent_node, "extra_kwargs", {})
|
|
209
|
+
if extra_kwargs:
|
|
210
|
+
arg_kwargs.update(extra_kwargs)
|
|
211
|
+
|
|
212
|
+
if not parent_node.required or parent_node.default is not None:
|
|
213
|
+
arg_kwargs["default"] = parent_node.default
|
|
214
|
+
|
|
215
|
+
func = click.argument(parent_node.name, **arg_kwargs)(func)
|
|
216
|
+
|
|
217
|
+
return func, h_flag_taken
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def as_decorator(
|
|
221
|
+
cls, name: str | None = None, /, **kwargs: Any
|
|
222
|
+
) -> Callable[[Callable[..., Any]], click.Command]:
|
|
223
|
+
"""
|
|
224
|
+
Return a decorator representation of the root node.
|
|
225
|
+
|
|
226
|
+
The root node is the top-level decorator that triggers tree building
|
|
227
|
+
and collects values from all parent nodes. When the decorated function
|
|
228
|
+
is called, it injects parent node values as keyword arguments.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
name (str, optional):
|
|
232
|
+
The name of the root node. If None, uses the decorated
|
|
233
|
+
function's name.
|
|
234
|
+
**kwargs (Any):
|
|
235
|
+
Additional keyword arguments for the specific root type.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Callable:
|
|
239
|
+
A decorator function that registers the root node
|
|
240
|
+
and builds the tree.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def decorator(func: Callable[..., Any]) -> Any:
|
|
244
|
+
"""The actual decorator that wraps the function."""
|
|
245
|
+
|
|
246
|
+
from click_extended.core.nodes.validation_node import ValidationNode
|
|
247
|
+
|
|
248
|
+
node_name = name if name is not None else func.__name__
|
|
249
|
+
root = cls(name=node_name, **kwargs)
|
|
250
|
+
root.tree.register_root(root)
|
|
251
|
+
original_func = func
|
|
252
|
+
|
|
253
|
+
if asyncio.iscoroutinefunction(func):
|
|
254
|
+
|
|
255
|
+
@wraps(func)
|
|
256
|
+
def sync_func(*sync_args: Any, **sync_kwargs: Any) -> Any:
|
|
257
|
+
"""Synchronous wrapper for async function."""
|
|
258
|
+
return asyncio.run(original_func(*sync_args, **sync_kwargs))
|
|
259
|
+
|
|
260
|
+
func = sync_func
|
|
261
|
+
|
|
262
|
+
@wraps(func)
|
|
263
|
+
def wrapper(*call_args: Any, **call_kwargs: Any) -> Any:
|
|
264
|
+
"""
|
|
265
|
+
Wrapper that executes the initialization phases
|
|
266
|
+
and injects values into the function.
|
|
267
|
+
|
|
268
|
+
Phases:
|
|
269
|
+
1. **Collection**: Already done (decorators applied).
|
|
270
|
+
2. **Context**: Initialize Click context with metadata.
|
|
271
|
+
3. **Validation**: Build and validate tree structure.
|
|
272
|
+
4. **Runtime**: Process parameters and execute function.
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
# Phase 1: Collection
|
|
276
|
+
context = click.get_current_context()
|
|
277
|
+
|
|
278
|
+
# Phase 2: Context
|
|
279
|
+
Tree.initialize_context(context, root)
|
|
280
|
+
|
|
281
|
+
# Phase 3: Validation
|
|
282
|
+
root.tree.validate_and_build(context)
|
|
283
|
+
|
|
284
|
+
# Phase 4: Runtime
|
|
285
|
+
if root.tree.root is None:
|
|
286
|
+
raise NoRootError()
|
|
287
|
+
|
|
288
|
+
parent_values: dict[str, Any] = {}
|
|
289
|
+
|
|
290
|
+
all_tag_names: set[str] = set()
|
|
291
|
+
for parent_node in root.tree.root.children.values():
|
|
292
|
+
if isinstance(
|
|
293
|
+
parent_node,
|
|
294
|
+
(OptionNode, ArgumentNode, type(parent_node)),
|
|
295
|
+
):
|
|
296
|
+
tags = parent_node.tags # type: ignore
|
|
297
|
+
all_tag_names.update(tags) # type: ignore
|
|
298
|
+
|
|
299
|
+
tags_dict: dict[str, "Tag"] = {}
|
|
300
|
+
for tag_name, tag in root.tree.tags.items():
|
|
301
|
+
tags_dict[tag_name] = tag
|
|
302
|
+
tag.parent_nodes = []
|
|
303
|
+
|
|
304
|
+
for tag_name in all_tag_names:
|
|
305
|
+
if tag_name not in tags_dict:
|
|
306
|
+
auto_tag = Tag(name=tag_name)
|
|
307
|
+
tags_dict[tag_name] = auto_tag
|
|
308
|
+
auto_tag.parent_nodes = []
|
|
309
|
+
|
|
310
|
+
for parent_node in root.tree.root.children.values():
|
|
311
|
+
if isinstance(
|
|
312
|
+
parent_node,
|
|
313
|
+
(OptionNode, ArgumentNode, type(parent_node)),
|
|
314
|
+
):
|
|
315
|
+
parent_node = cast("ParentNode", parent_node)
|
|
316
|
+
p_tags = parent_node.tags
|
|
317
|
+
for tag_name in p_tags:
|
|
318
|
+
if tag_name in tags_dict:
|
|
319
|
+
tags_dict[tag_name].parent_nodes.append(
|
|
320
|
+
parent_node
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
meta = context.meta.get("click_extended", {})
|
|
324
|
+
|
|
325
|
+
missing_env_vars: list[str] = []
|
|
326
|
+
for parent_node in root.tree.root.children.values():
|
|
327
|
+
if isinstance(parent_node, Env):
|
|
328
|
+
missing_var = parent_node.check_required()
|
|
329
|
+
if missing_var:
|
|
330
|
+
missing_env_vars.append(missing_var)
|
|
331
|
+
|
|
332
|
+
if missing_env_vars:
|
|
333
|
+
match len(missing_env_vars):
|
|
334
|
+
case 1:
|
|
335
|
+
error_msg = (
|
|
336
|
+
f"Required environment variable "
|
|
337
|
+
f"'{missing_env_vars[0]}' is not set."
|
|
338
|
+
)
|
|
339
|
+
case 2:
|
|
340
|
+
error_msg = (
|
|
341
|
+
f"Required environment variables "
|
|
342
|
+
f"'{missing_env_vars[0]}' and "
|
|
343
|
+
f"'{missing_env_vars[1]}' are not set."
|
|
344
|
+
)
|
|
345
|
+
case _:
|
|
346
|
+
vars_list = "', '".join(missing_env_vars[:-1])
|
|
347
|
+
error_msg = (
|
|
348
|
+
f"Required environment variables "
|
|
349
|
+
f"'{vars_list}' and "
|
|
350
|
+
f"'{missing_env_vars[-1]}' "
|
|
351
|
+
f"are not set."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
raise ProcessError(error_msg)
|
|
355
|
+
|
|
356
|
+
meta = context.meta.get("click_extended", {})
|
|
357
|
+
|
|
358
|
+
parents: dict[str, "ParentNode"] = {}
|
|
359
|
+
for name, node in root.tree.root.children.items():
|
|
360
|
+
if isinstance(name, str):
|
|
361
|
+
parents[name] = cast("ParentNode", node)
|
|
362
|
+
|
|
363
|
+
custom_context = Context(
|
|
364
|
+
root=root,
|
|
365
|
+
parent=None,
|
|
366
|
+
current=None,
|
|
367
|
+
click_context=context,
|
|
368
|
+
nodes={},
|
|
369
|
+
parents=parents,
|
|
370
|
+
tags=root.tree.tags,
|
|
371
|
+
children={},
|
|
372
|
+
data=meta.get("data", {}),
|
|
373
|
+
debug=meta.get("debug", False),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
for validation_node in root.tree.validations:
|
|
377
|
+
validation_node.on_init(
|
|
378
|
+
custom_context,
|
|
379
|
+
*validation_node.process_args,
|
|
380
|
+
**validation_node.process_kwargs,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
assert root.tree.root is not None
|
|
384
|
+
needs_async = False
|
|
385
|
+
for parent_node in root.tree.root.children.values():
|
|
386
|
+
if isinstance(
|
|
387
|
+
parent_node, (OptionNode, ArgumentNode, Env)
|
|
388
|
+
):
|
|
389
|
+
if (
|
|
390
|
+
parent_node.children
|
|
391
|
+
and check_has_async_handlers(
|
|
392
|
+
parent_node.children
|
|
393
|
+
)
|
|
394
|
+
):
|
|
395
|
+
needs_async = True
|
|
396
|
+
break
|
|
397
|
+
|
|
398
|
+
if not needs_async:
|
|
399
|
+
for tag in root.tree.tags.values():
|
|
400
|
+
if tag.children and check_has_async_handlers(
|
|
401
|
+
tag.children
|
|
402
|
+
):
|
|
403
|
+
needs_async = True
|
|
404
|
+
break
|
|
405
|
+
|
|
406
|
+
if needs_async:
|
|
407
|
+
|
|
408
|
+
async def async_processing() -> dict[str, Any]:
|
|
409
|
+
"""Process all handlers asynchronously."""
|
|
410
|
+
assert root.tree.root is not None
|
|
411
|
+
async_parent_values: dict[str, Any] = {}
|
|
412
|
+
|
|
413
|
+
# Phase 1
|
|
414
|
+
loaded_async_parents: dict[
|
|
415
|
+
str, tuple[Any, "ParentNode"]
|
|
416
|
+
] = {}
|
|
417
|
+
|
|
418
|
+
for (
|
|
419
|
+
parent_name,
|
|
420
|
+
parent_node,
|
|
421
|
+
) in root.tree.root.children.items():
|
|
422
|
+
if isinstance(parent_name, str):
|
|
423
|
+
raw_value = None
|
|
424
|
+
was_provided = False
|
|
425
|
+
|
|
426
|
+
if isinstance(
|
|
427
|
+
parent_node, (OptionNode, ArgumentNode)
|
|
428
|
+
):
|
|
429
|
+
raw_value = call_kwargs.get(parent_name)
|
|
430
|
+
was_provided = (
|
|
431
|
+
parent_name in call_kwargs
|
|
432
|
+
and raw_value != parent_node.default
|
|
433
|
+
)
|
|
434
|
+
parent_node.was_provided = was_provided
|
|
435
|
+
|
|
436
|
+
Tree.update_scope(
|
|
437
|
+
context,
|
|
438
|
+
"parent",
|
|
439
|
+
parent_node=parent_node,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
if asyncio.iscoroutinefunction(
|
|
443
|
+
parent_node.load
|
|
444
|
+
):
|
|
445
|
+
raw_value = await parent_node.load(
|
|
446
|
+
raw_value,
|
|
447
|
+
custom_context,
|
|
448
|
+
**parent_node.decorator_kwargs,
|
|
449
|
+
)
|
|
450
|
+
else:
|
|
451
|
+
raw_value = parent_node.load(
|
|
452
|
+
raw_value,
|
|
453
|
+
custom_context,
|
|
454
|
+
**parent_node.decorator_kwargs,
|
|
455
|
+
)
|
|
456
|
+
else:
|
|
457
|
+
parent_node = cast(
|
|
458
|
+
"ParentNode", parent_node
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
Tree.update_scope(
|
|
462
|
+
context,
|
|
463
|
+
"parent",
|
|
464
|
+
parent_node=parent_node,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
raw_value = (
|
|
468
|
+
await parent_node.load(
|
|
469
|
+
custom_context,
|
|
470
|
+
**parent_node.decorator_kwargs,
|
|
471
|
+
)
|
|
472
|
+
if asyncio.iscoroutinefunction(
|
|
473
|
+
parent_node.load
|
|
474
|
+
)
|
|
475
|
+
else parent_node.load(
|
|
476
|
+
custom_context,
|
|
477
|
+
**parent_node.decorator_kwargs,
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
was_provided = raw_value is not None
|
|
481
|
+
parent_node.was_provided = was_provided
|
|
482
|
+
|
|
483
|
+
inject_name = parent_node.param
|
|
484
|
+
parent_node.raw_value = raw_value
|
|
485
|
+
parent_node.cached_value = raw_value
|
|
486
|
+
|
|
487
|
+
loaded_async_parents[parent_name] = (
|
|
488
|
+
raw_value,
|
|
489
|
+
parent_node,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Phase 2
|
|
493
|
+
for parent_name, (
|
|
494
|
+
raw_value,
|
|
495
|
+
parent_node,
|
|
496
|
+
) in loaded_async_parents.items():
|
|
497
|
+
inject_name = parent_node.param
|
|
498
|
+
|
|
499
|
+
if parent_node.children:
|
|
500
|
+
Tree.update_scope(
|
|
501
|
+
context,
|
|
502
|
+
"parent",
|
|
503
|
+
parent_node=parent_node,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
processed_value = (
|
|
507
|
+
await process_children_async(
|
|
508
|
+
raw_value,
|
|
509
|
+
parent_node.children,
|
|
510
|
+
parent_node,
|
|
511
|
+
tags_dict,
|
|
512
|
+
context,
|
|
513
|
+
)
|
|
514
|
+
)
|
|
515
|
+
async_parent_values[inject_name] = (
|
|
516
|
+
processed_value
|
|
517
|
+
)
|
|
518
|
+
parent_node.cached_value = processed_value
|
|
519
|
+
else:
|
|
520
|
+
async_parent_values[inject_name] = raw_value
|
|
521
|
+
|
|
522
|
+
for tag in root.tree.tags.values():
|
|
523
|
+
if tag.children:
|
|
524
|
+
tag_values_dict = {
|
|
525
|
+
p.name: (p.get_value()) # type: ignore
|
|
526
|
+
for p in tag.parent_nodes
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
await process_children_async(
|
|
530
|
+
tag_values_dict,
|
|
531
|
+
tag.children,
|
|
532
|
+
tag,
|
|
533
|
+
tags_dict,
|
|
534
|
+
context,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
for validation_node in root.tree.validations:
|
|
538
|
+
if asyncio.iscoroutinefunction(
|
|
539
|
+
validation_node.on_finalize
|
|
540
|
+
):
|
|
541
|
+
await validation_node.on_finalize(
|
|
542
|
+
custom_context,
|
|
543
|
+
*validation_node.process_args,
|
|
544
|
+
**validation_node.process_kwargs,
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
validation_node.on_finalize(
|
|
548
|
+
custom_context,
|
|
549
|
+
*validation_node.process_args,
|
|
550
|
+
**validation_node.process_kwargs,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
return async_parent_values
|
|
554
|
+
|
|
555
|
+
try:
|
|
556
|
+
parent_values = asyncio.run(async_processing())
|
|
557
|
+
except RuntimeError as e:
|
|
558
|
+
if "already running" in str(e).lower():
|
|
559
|
+
raise ProcessError(
|
|
560
|
+
"Cannot use async handlers in an existing "
|
|
561
|
+
"event loop (e.g., Jupyter notebooks).",
|
|
562
|
+
tip="Use synchronous handlers instead, or "
|
|
563
|
+
"run your CLI outside of async contexts.",
|
|
564
|
+
) from e
|
|
565
|
+
raise
|
|
566
|
+
else:
|
|
567
|
+
# Phase 1
|
|
568
|
+
loaded_parents: dict[str, tuple[Any, "ParentNode"]] = {}
|
|
569
|
+
|
|
570
|
+
for (
|
|
571
|
+
parent_name,
|
|
572
|
+
parent_node,
|
|
573
|
+
) in root.tree.root.children.items():
|
|
574
|
+
if isinstance(parent_name, str):
|
|
575
|
+
raw_value = None
|
|
576
|
+
was_provided = False
|
|
577
|
+
|
|
578
|
+
if isinstance(
|
|
579
|
+
parent_node, (OptionNode, ArgumentNode)
|
|
580
|
+
):
|
|
581
|
+
raw_value = call_kwargs.get(parent_name)
|
|
582
|
+
was_provided = (
|
|
583
|
+
parent_name in call_kwargs
|
|
584
|
+
and raw_value != parent_node.default
|
|
585
|
+
)
|
|
586
|
+
parent_node.was_provided = was_provided
|
|
587
|
+
|
|
588
|
+
Tree.update_scope(
|
|
589
|
+
context,
|
|
590
|
+
"parent",
|
|
591
|
+
parent_node=parent_node,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
raw_value = parent_node.load(
|
|
595
|
+
raw_value,
|
|
596
|
+
custom_context,
|
|
597
|
+
**parent_node.decorator_kwargs,
|
|
598
|
+
)
|
|
599
|
+
else:
|
|
600
|
+
parent_node = cast(
|
|
601
|
+
"ParentNode", parent_node
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
Tree.update_scope(
|
|
605
|
+
context,
|
|
606
|
+
"parent",
|
|
607
|
+
parent_node=parent_node,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
raw_value = parent_node.load(
|
|
611
|
+
custom_context,
|
|
612
|
+
**parent_node.decorator_kwargs,
|
|
613
|
+
)
|
|
614
|
+
was_provided = raw_value is not None
|
|
615
|
+
parent_node.was_provided = was_provided
|
|
616
|
+
|
|
617
|
+
inject_name = parent_node.param
|
|
618
|
+
parent_node.raw_value = raw_value
|
|
619
|
+
parent_node.cached_value = raw_value
|
|
620
|
+
|
|
621
|
+
loaded_parents[parent_name] = (
|
|
622
|
+
raw_value,
|
|
623
|
+
parent_node,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# Phase 2
|
|
627
|
+
for parent_name, (
|
|
628
|
+
raw_value,
|
|
629
|
+
parent_node,
|
|
630
|
+
) in loaded_parents.items():
|
|
631
|
+
inject_name = parent_node.param
|
|
632
|
+
|
|
633
|
+
if parent_node.children:
|
|
634
|
+
Tree.update_scope(
|
|
635
|
+
context,
|
|
636
|
+
"parent",
|
|
637
|
+
parent_node=parent_node,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
processed_value = process_children(
|
|
641
|
+
raw_value,
|
|
642
|
+
parent_node.children,
|
|
643
|
+
parent_node,
|
|
644
|
+
tags_dict,
|
|
645
|
+
context,
|
|
646
|
+
)
|
|
647
|
+
parent_values[inject_name] = processed_value
|
|
648
|
+
parent_node.cached_value = processed_value
|
|
649
|
+
else:
|
|
650
|
+
parent_values[inject_name] = raw_value
|
|
651
|
+
|
|
652
|
+
for tag_name, tag in root.tree.tags.items():
|
|
653
|
+
if tag.children:
|
|
654
|
+
tag_values_dict = {
|
|
655
|
+
parent_node.name: parent_node.get_value()
|
|
656
|
+
for parent_node in tag.parent_nodes
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
process_children(
|
|
660
|
+
tag_values_dict,
|
|
661
|
+
tag.children,
|
|
662
|
+
tag,
|
|
663
|
+
tags_dict,
|
|
664
|
+
context,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
for validation_node in root.tree.validations:
|
|
668
|
+
validation_node.on_finalize(
|
|
669
|
+
custom_context,
|
|
670
|
+
*validation_node.process_args,
|
|
671
|
+
**validation_node.process_kwargs,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
merged_kwargs: dict[str, Any] = {
|
|
675
|
+
**call_kwargs,
|
|
676
|
+
**parent_values,
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
Tree.update_scope(context, "root")
|
|
680
|
+
|
|
681
|
+
return func(*call_args, **merged_kwargs)
|
|
682
|
+
except ContextAwareError as e:
|
|
683
|
+
e.show()
|
|
684
|
+
sys.exit(1)
|
|
685
|
+
except click.Abort:
|
|
686
|
+
raise
|
|
687
|
+
except Exception as e:
|
|
688
|
+
context = click.get_current_context()
|
|
689
|
+
meta = context.meta.get("click_extended", {})
|
|
690
|
+
debug = meta.get("debug", False)
|
|
691
|
+
|
|
692
|
+
child_node = meta.get("child_node")
|
|
693
|
+
child_node = cast("ChildNode | None", child_node)
|
|
694
|
+
|
|
695
|
+
parent_from_meta = meta.get("parent_node")
|
|
696
|
+
parent_from_meta = cast(
|
|
697
|
+
"ParentNode | None", parent_from_meta
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
root_node = meta.get("root_node")
|
|
701
|
+
root_node = cast("RootNode | None", root_node)
|
|
702
|
+
|
|
703
|
+
exc_name = e.__class__.__name__
|
|
704
|
+
exc_value = str(e)
|
|
705
|
+
|
|
706
|
+
if debug:
|
|
707
|
+
echo(
|
|
708
|
+
f"Exception '{exc_name}' caught:\n",
|
|
709
|
+
file=sys.stderr,
|
|
710
|
+
color=context.color,
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
lines: list[str] = [
|
|
714
|
+
f"Type: {exc_name}",
|
|
715
|
+
f"Message: {exc_value or 'None'}",
|
|
716
|
+
f"Function: {node_name}",
|
|
717
|
+
]
|
|
718
|
+
|
|
719
|
+
cond1 = child_node is not None
|
|
720
|
+
cond2 = parent_from_meta is not None
|
|
721
|
+
if cond1 and cond2:
|
|
722
|
+
assert parent_from_meta is not None
|
|
723
|
+
idx_list = [
|
|
724
|
+
int(k)
|
|
725
|
+
for k, v in parent_from_meta.children.items()
|
|
726
|
+
if id(v) == id(child_node)
|
|
727
|
+
]
|
|
728
|
+
current_index = idx_list[0]
|
|
729
|
+
|
|
730
|
+
# Handler:
|
|
731
|
+
handler_name: str = meta.get("handler_method", "")
|
|
732
|
+
handler_method = getattr(
|
|
733
|
+
child_node,
|
|
734
|
+
handler_name,
|
|
735
|
+
None,
|
|
736
|
+
)
|
|
737
|
+
lines.append(f"Handler: {handler_name}")
|
|
738
|
+
|
|
739
|
+
# Input value:
|
|
740
|
+
input_value = meta.get("handler_value", "")
|
|
741
|
+
lines.append(f"Input value: {input_value}")
|
|
742
|
+
|
|
743
|
+
# Input type:
|
|
744
|
+
input_type = humanize_type(type(input_value))
|
|
745
|
+
lines.append(f"Input type: {input_type}")
|
|
746
|
+
|
|
747
|
+
# Expected type:
|
|
748
|
+
expected_types_str = "Any"
|
|
749
|
+
handler_params = get_type_hints(handler_method)
|
|
750
|
+
|
|
751
|
+
if "value" in handler_params:
|
|
752
|
+
expected_type = handler_params["value"]
|
|
753
|
+
expected_type = cast(type, expected_type)
|
|
754
|
+
expected_types_str = humanize_type(
|
|
755
|
+
expected_type
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
lines.append(
|
|
759
|
+
f"Expected types: {expected_types_str}"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
# Previous:
|
|
763
|
+
has_previous = current_index > 0
|
|
764
|
+
if has_previous:
|
|
765
|
+
prev = parent_from_meta.children[
|
|
766
|
+
current_index - 1
|
|
767
|
+
]
|
|
768
|
+
else:
|
|
769
|
+
prev = parent_from_meta
|
|
770
|
+
previous_node = prev
|
|
771
|
+
lines.append(f"Previous: {repr(previous_node)}")
|
|
772
|
+
|
|
773
|
+
# Current:
|
|
774
|
+
lines.append(f"Current: {repr(child_node)}")
|
|
775
|
+
|
|
776
|
+
# Next:
|
|
777
|
+
children_len = len(parent_from_meta.children)
|
|
778
|
+
has_next = current_index < children_len - 1
|
|
779
|
+
if has_next:
|
|
780
|
+
next_node = parent_from_meta.children[
|
|
781
|
+
current_index + 1
|
|
782
|
+
]
|
|
783
|
+
else:
|
|
784
|
+
next_node = None
|
|
785
|
+
lines.append(f"Next: {repr(next_node)}")
|
|
786
|
+
|
|
787
|
+
elif parent_from_meta is not None:
|
|
788
|
+
lines.append(f"Parent: {repr(parent_from_meta)}")
|
|
789
|
+
|
|
790
|
+
if (
|
|
791
|
+
hasattr(parent_from_meta, "decorator_kwargs")
|
|
792
|
+
and parent_from_meta.decorator_kwargs
|
|
793
|
+
):
|
|
794
|
+
deckwargs = parent_from_meta.decorator_kwargs
|
|
795
|
+
lines.append(f"Parameters: {deckwargs}")
|
|
796
|
+
|
|
797
|
+
elif root_node is not None:
|
|
798
|
+
pass
|
|
799
|
+
|
|
800
|
+
# File
|
|
801
|
+
tracebacks = traceback.extract_tb(e.__traceback__)
|
|
802
|
+
frame = tracebacks[-1]
|
|
803
|
+
|
|
804
|
+
file_name = frame.filename
|
|
805
|
+
line_number = frame.lineno
|
|
806
|
+
|
|
807
|
+
lines.append(f"File: {file_name}")
|
|
808
|
+
lines.append(f"Line: {line_number}")
|
|
809
|
+
|
|
810
|
+
for line in lines:
|
|
811
|
+
echo(
|
|
812
|
+
line,
|
|
813
|
+
file=sys.stderr,
|
|
814
|
+
color=context.color,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
# Traceback
|
|
818
|
+
echo(
|
|
819
|
+
"\nTraceback:",
|
|
820
|
+
file=sys.stderr,
|
|
821
|
+
color=context.color,
|
|
822
|
+
)
|
|
823
|
+
tb_lines = traceback.format_exception(
|
|
824
|
+
type(e), e, e.__traceback__
|
|
825
|
+
)
|
|
826
|
+
for line in tb_lines[1:]:
|
|
827
|
+
echo(
|
|
828
|
+
line.rstrip(),
|
|
829
|
+
file=sys.stderr,
|
|
830
|
+
color=context.color,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
# Non-debug
|
|
834
|
+
else:
|
|
835
|
+
echo(
|
|
836
|
+
context.get_usage(),
|
|
837
|
+
file=sys.stderr,
|
|
838
|
+
color=context.color,
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
if context.command.get_help_option(context) is not None:
|
|
842
|
+
cmd = context.command_path
|
|
843
|
+
hint = f"Help: Try '{cmd} --help' for instructions."
|
|
844
|
+
echo(hint, file=sys.stderr, color=context.color)
|
|
845
|
+
|
|
846
|
+
if parent_from_meta is not None:
|
|
847
|
+
error_prefix = (
|
|
848
|
+
f"{exc_name} ({parent_from_meta.name})"
|
|
849
|
+
)
|
|
850
|
+
else:
|
|
851
|
+
error_prefix = exc_name
|
|
852
|
+
|
|
853
|
+
if exc_value == "":
|
|
854
|
+
message = f"{error_prefix}: Exception was raised."
|
|
855
|
+
else:
|
|
856
|
+
message = f"{error_prefix}: {exc_value}"
|
|
857
|
+
|
|
858
|
+
echo(
|
|
859
|
+
"\n" + message,
|
|
860
|
+
file=sys.stderr,
|
|
861
|
+
color=context.color,
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
sys.exit(1)
|
|
865
|
+
|
|
866
|
+
pending = list(reversed(Tree.get_pending_nodes()))
|
|
867
|
+
if root.tree.root is not None:
|
|
868
|
+
most_recent_parent = None
|
|
869
|
+
most_recent_tag = None
|
|
870
|
+
try:
|
|
871
|
+
for node_type, node in pending:
|
|
872
|
+
if node_type == "parent":
|
|
873
|
+
node = cast("ParentNode", node)
|
|
874
|
+
if node.name in root.tree.root.children:
|
|
875
|
+
from click_extended.errors import (
|
|
876
|
+
ParentExistsError,
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
raise ParentExistsError(node.name)
|
|
880
|
+
root.tree.root[node.name] = node
|
|
881
|
+
most_recent_parent = node
|
|
882
|
+
most_recent_tag = None
|
|
883
|
+
elif node_type == "child":
|
|
884
|
+
node = cast("ChildNode", node)
|
|
885
|
+
if most_recent_tag is not None:
|
|
886
|
+
if not root.tree.has_handle_tag_implemented(
|
|
887
|
+
node
|
|
888
|
+
):
|
|
889
|
+
tip = "".join(
|
|
890
|
+
"Children attached to @tag decorators "
|
|
891
|
+
"must implement the handle_tag(...) "
|
|
892
|
+
"method."
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
raise UnhandledTypeError(
|
|
896
|
+
child_name=node.name,
|
|
897
|
+
value_type="tag",
|
|
898
|
+
implemented_handlers=[],
|
|
899
|
+
tip=tip,
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
most_recent_tag[len(most_recent_tag)] = node
|
|
903
|
+
elif most_recent_parent is not None:
|
|
904
|
+
parent_len = len(most_recent_parent)
|
|
905
|
+
most_recent_parent[parent_len] = node
|
|
906
|
+
elif node_type == "tag":
|
|
907
|
+
tag_inst = cast(Tag, node)
|
|
908
|
+
root.tree.tags[tag_inst.name] = tag_inst
|
|
909
|
+
most_recent_tag = tag_inst
|
|
910
|
+
root.tree.recent_tag = tag_inst
|
|
911
|
+
elif node_type == "validation":
|
|
912
|
+
|
|
913
|
+
validation_inst = cast(ValidationNode, node)
|
|
914
|
+
root.tree.validations.append(validation_inst)
|
|
915
|
+
most_recent_tag = None
|
|
916
|
+
elif node_type == "child_validation":
|
|
917
|
+
child_val_inst = cast(ChildValidationNode, node)
|
|
918
|
+
if (
|
|
919
|
+
most_recent_tag is not None
|
|
920
|
+
or most_recent_parent is not None
|
|
921
|
+
):
|
|
922
|
+
if most_recent_tag is not None:
|
|
923
|
+
if not root.tree.has_handle_tag_implemented(
|
|
924
|
+
child_val_inst
|
|
925
|
+
):
|
|
926
|
+
tip = "".join(
|
|
927
|
+
"Child validation nodes "
|
|
928
|
+
"attached to @tag decorators "
|
|
929
|
+
"must implement the "
|
|
930
|
+
"handle_tag(...) method."
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
raise UnhandledTypeError(
|
|
934
|
+
child_name=child_val_inst.name,
|
|
935
|
+
value_type="tag",
|
|
936
|
+
implemented_handlers=[],
|
|
937
|
+
tip=tip,
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
most_recent_tag[len(most_recent_tag)] = (
|
|
941
|
+
child_val_inst
|
|
942
|
+
)
|
|
943
|
+
elif most_recent_parent is not None:
|
|
944
|
+
parent_len = len(most_recent_parent)
|
|
945
|
+
most_recent_parent[parent_len] = (
|
|
946
|
+
child_val_inst
|
|
947
|
+
)
|
|
948
|
+
else:
|
|
949
|
+
root.tree.validations.append(child_val_inst)
|
|
950
|
+
most_recent_tag = None
|
|
951
|
+
except ContextAwareError as e:
|
|
952
|
+
echo(
|
|
953
|
+
f"{e.__class__.__name__}: {e.message}", file=sys.stderr
|
|
954
|
+
)
|
|
955
|
+
if e.tip:
|
|
956
|
+
echo(f"Tip: {e.tip}", file=sys.stderr)
|
|
957
|
+
sys.exit(1)
|
|
958
|
+
|
|
959
|
+
return cls.wrap(wrapper, node_name, root, **kwargs)
|
|
960
|
+
|
|
961
|
+
return decorator
|
|
962
|
+
|
|
963
|
+
@classmethod
|
|
964
|
+
def wrap(
|
|
965
|
+
cls,
|
|
966
|
+
wrapped_func: Callable[..., Any],
|
|
967
|
+
name: str,
|
|
968
|
+
instance: "RootNode",
|
|
969
|
+
**kwargs: Any,
|
|
970
|
+
) -> click.Command: # type: ignore[return]
|
|
971
|
+
"""
|
|
972
|
+
Create the Click command/group object.
|
|
973
|
+
|
|
974
|
+
This method creates the actual Click command or group that will be
|
|
975
|
+
returned to the user, with full integration into
|
|
976
|
+
the `click-extended` system.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
wrapped_func (Callable):
|
|
980
|
+
The function already wrapped with value injection.
|
|
981
|
+
name (str):
|
|
982
|
+
The name of the root node.
|
|
983
|
+
instance (RootNode):
|
|
984
|
+
The `RootNode` instance that owns this tree.
|
|
985
|
+
**kwargs (Any):
|
|
986
|
+
Additional keyword arguments passed to the Click class.
|
|
987
|
+
|
|
988
|
+
Returns:
|
|
989
|
+
click.Command:
|
|
990
|
+
A `ClickCommand` or `ClickGroup` instance.
|
|
991
|
+
"""
|
|
992
|
+
func, h_flag_taken = cls._build_click_params(wrapped_func, instance)
|
|
993
|
+
|
|
994
|
+
if not h_flag_taken:
|
|
995
|
+
if "context_settings" not in kwargs:
|
|
996
|
+
kwargs["context_settings"] = {}
|
|
997
|
+
if "help_option_names" not in kwargs["context_settings"]:
|
|
998
|
+
kwargs["context_settings"]["help_option_names"] = [
|
|
999
|
+
"-h",
|
|
1000
|
+
"--help",
|
|
1001
|
+
]
|
|
1002
|
+
|
|
1003
|
+
click_cls = cls._get_click_cls()
|
|
1004
|
+
params = getattr(func, "__click_params__", [])
|
|
1005
|
+
|
|
1006
|
+
return click_cls(
|
|
1007
|
+
name=name,
|
|
1008
|
+
callback=func,
|
|
1009
|
+
params=params,
|
|
1010
|
+
root_instance=instance,
|
|
1011
|
+
**kwargs,
|
|
1012
|
+
)
|