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,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
+ )