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.
Files changed (146) hide show
  1. click_extended/core/__init__.py +10 -0
  2. click_extended/core/decorators/__init__.py +21 -0
  3. click_extended/core/decorators/argument.py +227 -0
  4. click_extended/core/decorators/command.py +93 -0
  5. click_extended/core/decorators/env.py +155 -0
  6. click_extended/core/decorators/group.py +96 -0
  7. click_extended/core/decorators/option.py +347 -0
  8. click_extended/core/decorators/prompt.py +69 -0
  9. click_extended/core/decorators/selection.py +155 -0
  10. click_extended/core/decorators/tag.py +109 -0
  11. click_extended/core/nodes/__init__.py +21 -0
  12. click_extended/core/nodes/_root_node.py +1012 -0
  13. click_extended/core/nodes/argument_node.py +165 -0
  14. click_extended/core/nodes/child_node.py +555 -0
  15. click_extended/core/nodes/child_validation_node.py +100 -0
  16. click_extended/core/nodes/node.py +55 -0
  17. click_extended/core/nodes/option_node.py +205 -0
  18. click_extended/core/nodes/parent_node.py +220 -0
  19. click_extended/core/nodes/validation_node.py +124 -0
  20. click_extended/core/other/__init__.py +7 -0
  21. click_extended/core/other/_click_command.py +60 -0
  22. click_extended/core/other/_click_group.py +246 -0
  23. click_extended/core/other/_tree.py +491 -0
  24. click_extended/core/other/context.py +496 -0
  25. click_extended/decorators/__init__.py +29 -0
  26. click_extended/decorators/check/__init__.py +57 -0
  27. click_extended/decorators/check/conflicts.py +149 -0
  28. click_extended/decorators/check/contains.py +69 -0
  29. click_extended/decorators/check/dependencies.py +115 -0
  30. click_extended/decorators/check/divisible_by.py +48 -0
  31. click_extended/decorators/check/ends_with.py +85 -0
  32. click_extended/decorators/check/exclusive.py +75 -0
  33. click_extended/decorators/check/falsy.py +37 -0
  34. click_extended/decorators/check/is_email.py +43 -0
  35. click_extended/decorators/check/is_hex_color.py +41 -0
  36. click_extended/decorators/check/is_hostname.py +47 -0
  37. click_extended/decorators/check/is_ipv4.py +46 -0
  38. click_extended/decorators/check/is_ipv6.py +46 -0
  39. click_extended/decorators/check/is_json.py +40 -0
  40. click_extended/decorators/check/is_mac_address.py +40 -0
  41. click_extended/decorators/check/is_negative.py +37 -0
  42. click_extended/decorators/check/is_non_zero.py +37 -0
  43. click_extended/decorators/check/is_port.py +39 -0
  44. click_extended/decorators/check/is_positive.py +37 -0
  45. click_extended/decorators/check/is_url.py +75 -0
  46. click_extended/decorators/check/is_uuid.py +40 -0
  47. click_extended/decorators/check/length.py +68 -0
  48. click_extended/decorators/check/not_empty.py +49 -0
  49. click_extended/decorators/check/regex.py +47 -0
  50. click_extended/decorators/check/requires.py +190 -0
  51. click_extended/decorators/check/starts_with.py +87 -0
  52. click_extended/decorators/check/truthy.py +37 -0
  53. click_extended/decorators/compare/__init__.py +15 -0
  54. click_extended/decorators/compare/at_least.py +57 -0
  55. click_extended/decorators/compare/at_most.py +57 -0
  56. click_extended/decorators/compare/between.py +119 -0
  57. click_extended/decorators/compare/greater_than.py +183 -0
  58. click_extended/decorators/compare/less_than.py +183 -0
  59. click_extended/decorators/convert/__init__.py +31 -0
  60. click_extended/decorators/convert/convert_angle.py +94 -0
  61. click_extended/decorators/convert/convert_area.py +123 -0
  62. click_extended/decorators/convert/convert_bits.py +211 -0
  63. click_extended/decorators/convert/convert_distance.py +154 -0
  64. click_extended/decorators/convert/convert_energy.py +155 -0
  65. click_extended/decorators/convert/convert_power.py +128 -0
  66. click_extended/decorators/convert/convert_pressure.py +131 -0
  67. click_extended/decorators/convert/convert_speed.py +122 -0
  68. click_extended/decorators/convert/convert_temperature.py +89 -0
  69. click_extended/decorators/convert/convert_time.py +108 -0
  70. click_extended/decorators/convert/convert_volume.py +218 -0
  71. click_extended/decorators/convert/convert_weight.py +158 -0
  72. click_extended/decorators/load/__init__.py +13 -0
  73. click_extended/decorators/load/load_csv.py +117 -0
  74. click_extended/decorators/load/load_json.py +61 -0
  75. click_extended/decorators/load/load_toml.py +47 -0
  76. click_extended/decorators/load/load_yaml.py +72 -0
  77. click_extended/decorators/math/__init__.py +37 -0
  78. click_extended/decorators/math/absolute.py +35 -0
  79. click_extended/decorators/math/add.py +48 -0
  80. click_extended/decorators/math/ceil.py +36 -0
  81. click_extended/decorators/math/clamp.py +51 -0
  82. click_extended/decorators/math/divide.py +42 -0
  83. click_extended/decorators/math/floor.py +36 -0
  84. click_extended/decorators/math/maximum.py +39 -0
  85. click_extended/decorators/math/minimum.py +39 -0
  86. click_extended/decorators/math/modulo.py +39 -0
  87. click_extended/decorators/math/multiply.py +51 -0
  88. click_extended/decorators/math/normalize.py +76 -0
  89. click_extended/decorators/math/power.py +39 -0
  90. click_extended/decorators/math/rounded.py +39 -0
  91. click_extended/decorators/math/sqrt.py +39 -0
  92. click_extended/decorators/math/subtract.py +39 -0
  93. click_extended/decorators/math/to_percent.py +63 -0
  94. click_extended/decorators/misc/__init__.py +17 -0
  95. click_extended/decorators/misc/choice.py +139 -0
  96. click_extended/decorators/misc/confirm_if.py +147 -0
  97. click_extended/decorators/misc/default.py +95 -0
  98. click_extended/decorators/misc/deprecated.py +131 -0
  99. click_extended/decorators/misc/experimental.py +79 -0
  100. click_extended/decorators/misc/now.py +42 -0
  101. click_extended/decorators/random/__init__.py +21 -0
  102. click_extended/decorators/random/random_bool.py +49 -0
  103. click_extended/decorators/random/random_choice.py +63 -0
  104. click_extended/decorators/random/random_datetime.py +140 -0
  105. click_extended/decorators/random/random_float.py +62 -0
  106. click_extended/decorators/random/random_integer.py +56 -0
  107. click_extended/decorators/random/random_prime.py +196 -0
  108. click_extended/decorators/random/random_string.py +77 -0
  109. click_extended/decorators/random/random_uuid.py +119 -0
  110. click_extended/decorators/transform/__init__.py +71 -0
  111. click_extended/decorators/transform/add_prefix.py +58 -0
  112. click_extended/decorators/transform/add_suffix.py +58 -0
  113. click_extended/decorators/transform/apply.py +35 -0
  114. click_extended/decorators/transform/basename.py +44 -0
  115. click_extended/decorators/transform/dirname.py +44 -0
  116. click_extended/decorators/transform/expand_vars.py +36 -0
  117. click_extended/decorators/transform/remove_prefix.py +57 -0
  118. click_extended/decorators/transform/remove_suffix.py +57 -0
  119. click_extended/decorators/transform/replace.py +46 -0
  120. click_extended/decorators/transform/slugify.py +45 -0
  121. click_extended/decorators/transform/split.py +43 -0
  122. click_extended/decorators/transform/strip.py +148 -0
  123. click_extended/decorators/transform/to_case.py +216 -0
  124. click_extended/decorators/transform/to_date.py +75 -0
  125. click_extended/decorators/transform/to_datetime.py +83 -0
  126. click_extended/decorators/transform/to_path.py +274 -0
  127. click_extended/decorators/transform/to_time.py +77 -0
  128. click_extended/decorators/transform/to_timestamp.py +114 -0
  129. click_extended/decorators/transform/truncate.py +47 -0
  130. click_extended/utils/__init__.py +13 -0
  131. click_extended/utils/casing.py +169 -0
  132. click_extended/utils/checks.py +48 -0
  133. click_extended/utils/dispatch.py +1016 -0
  134. click_extended/utils/format.py +101 -0
  135. click_extended/utils/humanize.py +209 -0
  136. click_extended/utils/naming.py +238 -0
  137. click_extended/utils/process.py +294 -0
  138. click_extended/utils/selection.py +267 -0
  139. click_extended/utils/time.py +46 -0
  140. {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/METADATA +2 -1
  141. click_extended-1.0.1.dist-info/RECORD +149 -0
  142. click_extended-1.0.0.dist-info/RECORD +0 -10
  143. {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/WHEEL +0 -0
  144. {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/licenses/AUTHORS.md +0 -0
  145. {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/licenses/LICENSE +0 -0
  146. {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1016 @@
1
+ """
2
+ Dispatcher utilities for child node handler routing.
3
+
4
+ This module provides the core dispatching logic that routes values to
5
+ appropriate child node handlers based on type, context, and handler
6
+ availability. It supports both synchronous and asynchronous handlers with
7
+ automatic detection and routing.
8
+ """
9
+
10
+ # pylint: disable=too-many-return-statements
11
+ # pylint: disable=too-many-branches
12
+ # pylint: disable=too-many-statements
13
+ # pylint: disable=too-many-locals
14
+ # pylint: disable=too-many-nested-blocks
15
+ # pylint: disable=import-outside-toplevel
16
+ # pylint: disable=too-many-lines
17
+
18
+ import asyncio
19
+ from datetime import date, datetime, time
20
+ from decimal import Decimal
21
+ from pathlib import Path
22
+ from types import UnionType
23
+ from typing import (
24
+ TYPE_CHECKING,
25
+ Any,
26
+ Union,
27
+ cast,
28
+ get_args,
29
+ get_origin,
30
+ get_type_hints,
31
+ )
32
+ from uuid import UUID
33
+
34
+ from click_extended.core.nodes.child_node import ChildNode
35
+ from click_extended.errors import (
36
+ InvalidHandlerError,
37
+ ProcessError,
38
+ UnhandledTypeError,
39
+ )
40
+
41
+ if TYPE_CHECKING:
42
+ from click_extended.core.other.context import Context
43
+
44
+
45
+ TYPE_SPECIFIC_HANDLERS = [
46
+ "handle_str",
47
+ "handle_int",
48
+ "handle_float",
49
+ "handle_bool",
50
+ "handle_numeric",
51
+ "handle_list",
52
+ "handle_dict",
53
+ "handle_tuple",
54
+ "handle_path",
55
+ "handle_uuid",
56
+ "handle_datetime",
57
+ "handle_date",
58
+ "handle_time",
59
+ "handle_bytes",
60
+ "handle_decimal",
61
+ ]
62
+
63
+ ALL_HANDLER_NAMES = [
64
+ "handle_all",
65
+ "handle_none",
66
+ *TYPE_SPECIFIC_HANDLERS,
67
+ "handle_tag",
68
+ ]
69
+
70
+
71
+ def _extract_inner_types(type_hint: Any) -> set[type]:
72
+ """
73
+ Extract the expected types from a type hint.
74
+
75
+ Examples:
76
+ - `int` -> `{int}`
77
+ - `int | str` -> `{int, str}`
78
+ - `tuple[int, ...]` -> `{int}`
79
+ - `tuple[int | str, ...]` -> `{int, str}`
80
+ - `tuple[tuple[int, ...], ...]` -> `{int}` (from innermost)
81
+ - `list[str]` -> `{str}`
82
+
83
+ Args:
84
+ type_hint (Any):
85
+ The type hint to extract from.
86
+
87
+ Returns:
88
+ set[type]:
89
+ Set of expected types.
90
+ """
91
+ if type_hint is Any:
92
+ return set()
93
+
94
+ origin = get_origin(type_hint)
95
+ args = get_args(type_hint)
96
+
97
+ if origin is UnionType or (
98
+ origin is not None and str(origin).startswith("typing.Union")
99
+ ):
100
+ result: set[type] = set()
101
+ for arg in args:
102
+ if arg is type(None) or arg is Any:
103
+ continue
104
+ result.update(_extract_inner_types(arg))
105
+ return result
106
+
107
+ # tuple[T, ...]
108
+ if origin is tuple and args:
109
+ if args[-1] is Ellipsis:
110
+ return _extract_inner_types(args[0])
111
+
112
+ result = set()
113
+ for arg in args:
114
+ if arg is not Any:
115
+ result.update(_extract_inner_types(arg))
116
+ return result
117
+
118
+ # list[T], set[T], etc.
119
+ if origin in (list, set, frozenset) and args:
120
+ if args[0] is Any:
121
+ return set()
122
+ return _extract_inner_types(args[0])
123
+
124
+ # dict[K, V]
125
+ if origin is dict and len(args) >= 2:
126
+ if args[1] is Any:
127
+ return set()
128
+ return _extract_inner_types(args[1])
129
+
130
+ if isinstance(type_hint, type):
131
+ return {type_hint}
132
+
133
+ return set()
134
+
135
+
136
+ def _validate_handler_type(
137
+ handler_name: str, value: Any, type_hint: Any
138
+ ) -> tuple[bool, str]:
139
+ """
140
+ Validate that value matches handler's type hint.
141
+
142
+ Args:
143
+ handler_name (str):
144
+ Name of the handler being called.
145
+ value (Any):
146
+ The runtime value to validate.
147
+ type_hint (Any):
148
+ The type hint from the handler's signature.
149
+
150
+ Returns:
151
+ tuple[bool, str]:
152
+ `(is_valid, error_message)` where `error_message` is empty if valid.
153
+ """
154
+ origin = get_origin(type_hint)
155
+
156
+ if type_hint is Any:
157
+ return True, ""
158
+
159
+ if isinstance(type_hint, type) and origin is None:
160
+ if not isinstance(value, type_hint):
161
+ expected_name = type_hint.__name__
162
+ actual_type = type(value).__name__
163
+ suggestion = ""
164
+
165
+ if actual_type == "str" and type_hint in (int, float):
166
+ suggestion = (
167
+ "\nTip: Add type=int to your option/argument "
168
+ "to convert strings to integers."
169
+ )
170
+ elif actual_type == "int" and type_hint == str:
171
+ suggestion = (
172
+ "\nTip: Change type=int to type=str in "
173
+ "your option/argument."
174
+ )
175
+
176
+ return (
177
+ False,
178
+ f"Expected {expected_name}, got {actual_type}.{suggestion}",
179
+ )
180
+
181
+ if handler_name in (
182
+ "handle_str",
183
+ "handle_int",
184
+ "handle_float",
185
+ "handle_bool",
186
+ "handle_numeric",
187
+ ):
188
+ expected_type_map: dict[str, type | tuple[type, ...]] = {
189
+ "handle_str": str,
190
+ "handle_int": int,
191
+ "handle_float": float,
192
+ "handle_bool": bool,
193
+ "handle_numeric": (int, float),
194
+ }
195
+ expected = expected_type_map[handler_name]
196
+
197
+ if not isinstance(value, expected):
198
+ if handler_name == "handle_numeric":
199
+ return (
200
+ False,
201
+ f"Expected int or float, got {type(value).__name__}",
202
+ )
203
+ expected_name = (
204
+ expected.__name__ if isinstance(expected, type) else "number"
205
+ )
206
+ actual_type = type(value).__name__
207
+ suggestion = ""
208
+
209
+ if actual_type == "str" and expected in (int, (int, float)):
210
+ suggestion = (
211
+ "\nTip: Add type=int to your option/argument "
212
+ "to convert strings to integers."
213
+ )
214
+ elif actual_type == "int" and expected == str:
215
+ suggestion = (
216
+ "\nTip: Change type=int to type=str in "
217
+ "your option/argument."
218
+ )
219
+
220
+ return (
221
+ False,
222
+ f"Expected {expected_name}, got {actual_type}.{suggestion}",
223
+ )
224
+
225
+ elif handler_name == "handle_tuple":
226
+ if not isinstance(value, tuple):
227
+ return False, f"Expected tuple, got {type(value).__name__}"
228
+
229
+ elif handler_name == "handle_list":
230
+ if not isinstance(value, list):
231
+ return False, f"Expected list, got {type(value).__name__}"
232
+
233
+ expected_types = _extract_inner_types(type_hint)
234
+ if expected_types and value:
235
+ list_mismatches: list[tuple[int, str, Any]] = []
236
+ value = cast(Any, value)
237
+ for i, item in enumerate(value):
238
+ if not any(isinstance(item, t) for t in expected_types):
239
+ list_mismatches.append((i, type(item).__name__, item))
240
+
241
+ if list_mismatches:
242
+ type_names = " | ".join(
243
+ sorted(t.__name__ for t in expected_types)
244
+ )
245
+
246
+ examples: list[str] = []
247
+ for (
248
+ _,
249
+ item_type,
250
+ item_val,
251
+ ) in list_mismatches[:3]:
252
+ examples.append(f"{repr(item_val)} ({item_type})")
253
+
254
+ error_msg = "".join(
255
+ f"Expected list[{type_names}], but found "
256
+ f"{len(list_mismatches)} item(s) with wrong type."
257
+ f"\nExamples: {', '.join(examples)}"
258
+ )
259
+
260
+ return False, error_msg
261
+
262
+ elif handler_name == "handle_dict":
263
+ if not isinstance(value, dict):
264
+ return False, f"Expected dict, got {type(value).__name__}"
265
+
266
+ return True, ""
267
+
268
+
269
+ def dispatch_to_child(
270
+ child: "ChildNode",
271
+ value: Any,
272
+ context: "Context",
273
+ ) -> Any:
274
+ """
275
+ Dispatch value to appropriate child handler with priority system.
276
+
277
+ Args:
278
+ child (ChildNode):
279
+ The child node to dispatch to.
280
+ value (Any):
281
+ The value to process.
282
+ context (Context):
283
+ Processing context with parent, siblings, tags.
284
+
285
+ Returns:
286
+ Any:
287
+ The processed value. Returns original value if the
288
+ handler returns `None`.
289
+
290
+ Raises:
291
+ UnhandledTypeError:
292
+ If no handler is implemented for this value type.
293
+ InvalidHandlerError:
294
+ If `handle_tag` returns a modified dictionary.
295
+ """
296
+ if isinstance(value, tuple):
297
+ meta = context.click_context.meta.get("click_extended", {})
298
+ is_container = meta.get("is_container_tuple", False)
299
+
300
+ if is_container:
301
+ return _process_container_tuple(
302
+ child,
303
+ value, # type: ignore
304
+ context,
305
+ )
306
+
307
+ if value is None:
308
+ # Handle None
309
+ if _is_handler_implemented(child, "handle_none"):
310
+ try:
311
+ result = child.handle_none(
312
+ context, *child.process_args, **child.process_kwargs
313
+ )
314
+ return value if result is None else result # type: ignore
315
+ except NotImplementedError:
316
+ pass
317
+
318
+ for handler_name in TYPE_SPECIFIC_HANDLERS:
319
+ if _is_handler_implemented(child, handler_name):
320
+ if _should_call_handler(child, handler_name, value):
321
+ try:
322
+ handler = getattr(child, handler_name)
323
+ result = handler(
324
+ value,
325
+ context,
326
+ *child.process_args,
327
+ **child.process_kwargs,
328
+ )
329
+
330
+ if result is None:
331
+ return value
332
+ return result
333
+ except NotImplementedError:
334
+ pass
335
+
336
+ # Handle all
337
+ try:
338
+ if _should_call_handler(child, "handle_all", value):
339
+ result = child.handle_all(
340
+ value, context, *child.process_args, **child.process_kwargs
341
+ )
342
+ return value if result is None else result
343
+ except NotImplementedError:
344
+ pass
345
+
346
+ return None
347
+
348
+ handler_name = _determine_handler(
349
+ child,
350
+ value,
351
+ context,
352
+ ) # type: ignore[assignment]
353
+
354
+ # Handle specific
355
+ if handler_name:
356
+ try:
357
+ if _should_call_handler(child, handler_name, value):
358
+ if "click_extended" in context.click_context.meta:
359
+ context.click_context.meta["click_extended"][
360
+ "handler_method"
361
+ ] = handler_name # type: ignore[assignment]
362
+
363
+ handler = getattr(child, handler_name)
364
+ hints = get_type_hints(handler)
365
+
366
+ if "value" in hints:
367
+ is_valid, error_msg = _validate_handler_type(
368
+ handler_name, value, hints["value"]
369
+ )
370
+ if not is_valid:
371
+ raise ProcessError(
372
+ f"Type mismatch in {handler_name}: " f"{error_msg}"
373
+ )
374
+
375
+ result = handler(
376
+ value, context, *child.process_args, **child.process_kwargs
377
+ )
378
+
379
+ if handler_name == "handle_tag" and result is not None:
380
+ message = (
381
+ "Method handle_tag() is validation-only and "
382
+ "does not support transformations."
383
+ )
384
+
385
+ tip = (
386
+ "Remove the return statement to make it "
387
+ "validation-only or move the "
388
+ "transformation logic to the parent node."
389
+ )
390
+
391
+ raise InvalidHandlerError(message=message, tip=tip)
392
+
393
+ return value if result is None else result # type: ignore
394
+ except NotImplementedError:
395
+ pass
396
+
397
+ try:
398
+ if _should_call_handler(child, "handle_all", value):
399
+ if "click_extended" in context.click_context.meta:
400
+ context.click_context.meta["click_extended"][
401
+ "handler_method"
402
+ ] = "handle_all"
403
+
404
+ result = child.handle_all(
405
+ value, context, *child.process_args, **child.process_kwargs
406
+ )
407
+ return value if result is None else result # type: ignore
408
+ except NotImplementedError:
409
+ pass
410
+
411
+ raise UnhandledTypeError(
412
+ child_name=child.name,
413
+ value_type=type(value).__name__, # type: ignore
414
+ implemented_handlers=_get_implemented_handlers(child),
415
+ )
416
+
417
+
418
+ def _determine_handler(
419
+ child: "ChildNode", value: Any, context: "Context"
420
+ ) -> str | None:
421
+ """
422
+ Determine which handler should process this value based on priority.
423
+
424
+ Args:
425
+ child (ChildNode):
426
+ The child node to check for implemented handlers.
427
+ value (Any):
428
+ The value to check.
429
+ context (Context):
430
+ The processing context.
431
+
432
+ Returns:
433
+ str | None:
434
+ Handler method name, or `None` if no handler found.
435
+ """
436
+ if context.is_tag() and _is_handler_implemented(child, "handle_tag"):
437
+ return "handle_tag"
438
+
439
+ if isinstance(value, bytes):
440
+ if _is_handler_implemented(child, "handle_bytes"):
441
+ return "handle_bytes"
442
+ elif isinstance(value, Decimal):
443
+ if _is_handler_implemented(child, "handle_decimal"):
444
+ return "handle_decimal"
445
+ elif isinstance(value, datetime):
446
+ if _is_handler_implemented(child, "handle_datetime"):
447
+ return "handle_datetime"
448
+ elif isinstance(value, date):
449
+ if _is_handler_implemented(child, "handle_date"):
450
+ return "handle_date"
451
+ elif isinstance(value, time):
452
+ if _is_handler_implemented(child, "handle_time"):
453
+ return "handle_time"
454
+ elif isinstance(value, UUID):
455
+ if _is_handler_implemented(child, "handle_uuid"):
456
+ return "handle_uuid"
457
+ elif isinstance(value, Path):
458
+ if _is_handler_implemented(child, "handle_path"):
459
+ return "handle_path"
460
+ elif isinstance(value, dict):
461
+ if _is_handler_implemented(child, "handle_dict"):
462
+ return "handle_dict"
463
+ elif isinstance(value, str):
464
+ if _is_handler_implemented(child, "handle_str"):
465
+ return "handle_str"
466
+ elif isinstance(
467
+ value, bool
468
+ ): # Must check bool before int since bool is subclass of int
469
+ if _is_handler_implemented(child, "handle_bool"):
470
+ return "handle_bool"
471
+ elif isinstance(value, int):
472
+ if _is_handler_implemented(child, "handle_int"):
473
+ return "handle_int"
474
+ if _is_handler_implemented(child, "handle_numeric"):
475
+ return "handle_numeric"
476
+ elif isinstance(value, float):
477
+ if _is_handler_implemented(child, "handle_float"):
478
+ return "handle_float"
479
+ if _is_handler_implemented(child, "handle_numeric"):
480
+ return "handle_numeric"
481
+ elif isinstance(value, list):
482
+ if _is_handler_implemented(child, "handle_list"):
483
+ return "handle_list"
484
+ elif isinstance(value, tuple):
485
+ if _is_handler_implemented(child, "handle_tuple"):
486
+ return "handle_tuple"
487
+ return None
488
+
489
+ if _is_handler_implemented(child, "handle_all"):
490
+ return "handle_all"
491
+
492
+ return None
493
+
494
+
495
+ def _should_call_handler(
496
+ child: "ChildNode", handler_name: str, value: Any
497
+ ) -> bool:
498
+ """
499
+ Check if handler should be called for this value.
500
+
501
+ Checks type hints to see if `None` values are accepted.
502
+
503
+ Args:
504
+ child (ChildNode):
505
+ The child node.
506
+ handler_name (str):
507
+ Name of the handler method.
508
+ value (Any):
509
+ The value to check.
510
+
511
+ Returns:
512
+ bool:
513
+ `True` if handler should be called, `False` if
514
+ value should be skipped.
515
+ """
516
+ if value is not None:
517
+ return True
518
+
519
+ try:
520
+ method = getattr(child, handler_name, None)
521
+ if method is None:
522
+ return False
523
+
524
+ hints = get_type_hints(method)
525
+ if "value" not in hints:
526
+ return True
527
+
528
+ value_hint = hints["value"]
529
+
530
+ if value_hint is Any:
531
+ return True
532
+
533
+ origin = get_origin(value_hint)
534
+
535
+ if origin is UnionType:
536
+ args = get_args(value_hint)
537
+ return type(None) in args
538
+
539
+ if origin is Union:
540
+ args = get_args(value_hint)
541
+ return type(None) in args
542
+
543
+ return False
544
+ except (AttributeError, ImportError):
545
+ return True
546
+
547
+
548
+ def _is_handler_implemented(child: "ChildNode", handler_name: str) -> bool:
549
+ """
550
+ Check if a handler is implemented by the child (not just inherited).
551
+
552
+ Args:
553
+ child (ChildNode):
554
+ The child node instance.
555
+ handler_name (str):
556
+ Name of the handler method to check.
557
+
558
+ Returns:
559
+ bool:
560
+ `True` if handler is implemented by child class, `False` otherwise.
561
+ """
562
+ for cls in type(child).__mro__:
563
+ if handler_name in cls.__dict__:
564
+ return cls is not ChildNode
565
+
566
+ return False
567
+
568
+
569
+ def _get_implemented_handlers(child: "ChildNode") -> list[str]:
570
+ """
571
+ Get list of implemented handler names by checking class hierarchy.
572
+
573
+ Args:
574
+ child (ChildNode):
575
+ The child node instance.
576
+
577
+ Returns:
578
+ list[str]:
579
+ List of handler names (without `'handle_'` prefix)
580
+ that are implemented.
581
+ """
582
+ handlers: list[str] = []
583
+
584
+ for handler_name in ALL_HANDLER_NAMES:
585
+ for cls in type(child).__mro__:
586
+ if handler_name in cls.__dict__:
587
+ if cls is not ChildNode:
588
+ handlers.append(handler_name.replace("handle_", ""))
589
+ break
590
+
591
+ return handlers
592
+
593
+
594
+ def _process_container_tuple(
595
+ child: "ChildNode",
596
+ value: tuple[Any, ...],
597
+ context: "Context",
598
+ path: list[int] | None = None,
599
+ ) -> tuple[Any, ...]:
600
+ """
601
+ Process a container tuple by applying handlers to each element in-place.
602
+
603
+ This function recursively processes tuples from options/arguments with
604
+ `multiple=True` or `nargs>1`, applying appropriate handlers to each
605
+ leaf element based on its type and preserving the tuple structure.
606
+
607
+ Args:
608
+ child (ChildNode):
609
+ The child node to dispatch handlers from.
610
+ value (tuple[Any, ...]):
611
+ The container tuple to process.
612
+ context (Context):
613
+ Processing context.
614
+ path (list[int] | None):
615
+ Current path for error reporting. Defaults to empty list.
616
+
617
+ Returns:
618
+ tuple[Any, ...]:
619
+ New tuple with same structure but processed elements.
620
+
621
+ Raises:
622
+ ValueError:
623
+ If validation fails, with path information added.
624
+ TypeError:
625
+ If type mismatch occurs, with path information added.
626
+ UnhandledTypeError:
627
+ If no handler exists for an element's type.
628
+ """
629
+ if path is None:
630
+ path = []
631
+
632
+ results: list[Any] = []
633
+
634
+ for i, item in enumerate(value):
635
+ current_path = path + [i]
636
+
637
+ try:
638
+ if isinstance(item, tuple):
639
+ result = _process_container_tuple(
640
+ child,
641
+ item, # type: ignore
642
+ context,
643
+ current_path,
644
+ )
645
+ else:
646
+ if handler_name := _determine_handler(child, item, context):
647
+ if _should_call_handler(child, handler_name, item):
648
+ handler = getattr(child, handler_name)
649
+ result = handler(
650
+ item,
651
+ context,
652
+ *child.process_args,
653
+ **child.process_kwargs,
654
+ )
655
+ else:
656
+ result = item
657
+ elif _is_handler_implemented(child, "handle_all"):
658
+ if _should_call_handler(child, "handle_all", item):
659
+ result = child.handle_all(
660
+ item,
661
+ context,
662
+ *child.process_args,
663
+ **child.process_kwargs,
664
+ )
665
+ else:
666
+ result = item
667
+ else:
668
+ raise UnhandledTypeError(
669
+ child_name=child.name,
670
+ value_type=type(item).__name__, # type: ignore
671
+ implemented_handlers=_get_implemented_handlers(child),
672
+ )
673
+
674
+ results.append(result)
675
+
676
+ except (ValueError, TypeError) as e:
677
+ path_str = "".join(f"[{idx}]" for idx in current_path)
678
+ error_msg = str(e)
679
+ if path_str and " at index " not in error_msg:
680
+ raise type(e)(f"{error_msg} at index {path_str}") from e
681
+ raise
682
+
683
+ return tuple(results)
684
+
685
+
686
+ async def _process_container_tuple_async(
687
+ child: "ChildNode",
688
+ value: tuple[Any, ...],
689
+ context: "Context",
690
+ path: list[int] | None = None,
691
+ ) -> tuple[Any, ...]:
692
+ """
693
+ Async version of _process_container_tuple for async handler support.
694
+
695
+ Process a container tuple by applying handlers to each element in-place.
696
+
697
+ This function recursively processes tuples from options/arguments with
698
+ `multiple=True` or `nargs>1`, applying appropriate handlers to each
699
+ leaf element based on its type and preserving the tuple structure.
700
+
701
+ Args:
702
+ child (ChildNode):
703
+ The child node to dispatch handlers from.
704
+ value (tuple[Any, ...]):
705
+ The container tuple to process.
706
+ context (Context):
707
+ Processing context.
708
+ path (list[int] | None):
709
+ Current path for error reporting. Defaults to empty list.
710
+
711
+ Returns:
712
+ tuple[Any, ...]:
713
+ New tuple with same structure but processed elements.
714
+
715
+ Raises:
716
+ ValueError:
717
+ If validation fails, with path information added.
718
+ TypeError:
719
+ If type mismatch occurs, with path information added.
720
+ UnhandledTypeError:
721
+ If no handler exists for an element's type.
722
+ """
723
+ if path is None:
724
+ path = []
725
+
726
+ results: list[Any] = []
727
+
728
+ for i, item in enumerate(value):
729
+ current_path = path + [i]
730
+
731
+ try:
732
+ if isinstance(item, tuple):
733
+ result = await _process_container_tuple_async(
734
+ child,
735
+ item, # type: ignore
736
+ context,
737
+ current_path,
738
+ )
739
+ else:
740
+ if handler_name := _determine_handler(child, item, context):
741
+ if _should_call_handler(child, handler_name, item):
742
+ handler = getattr(child, handler_name)
743
+ if asyncio.iscoroutinefunction(handler):
744
+ result = await handler(
745
+ item,
746
+ context,
747
+ *child.process_args,
748
+ **child.process_kwargs,
749
+ )
750
+ else:
751
+ result = handler(
752
+ item,
753
+ context,
754
+ *child.process_args,
755
+ **child.process_kwargs,
756
+ )
757
+ else:
758
+ result = item
759
+ elif _is_handler_implemented(child, "handle_all"):
760
+ if _should_call_handler(child, "handle_all", item):
761
+ handler = child.handle_all
762
+ if asyncio.iscoroutinefunction(handler):
763
+ result = await handler(
764
+ item,
765
+ context,
766
+ *child.process_args,
767
+ **child.process_kwargs,
768
+ )
769
+ else:
770
+ result = handler(
771
+ item,
772
+ context,
773
+ *child.process_args,
774
+ **child.process_kwargs,
775
+ )
776
+ else:
777
+ result = item
778
+ else:
779
+ raise UnhandledTypeError(
780
+ child_name=child.name,
781
+ value_type=type(item).__name__, # type: ignore
782
+ implemented_handlers=_get_implemented_handlers(child),
783
+ )
784
+
785
+ results.append(result)
786
+
787
+ except (ValueError, TypeError) as e:
788
+ path_str = "".join(f"[{idx}]" for idx in current_path)
789
+ error_msg = str(e)
790
+ if path_str and " at index " not in error_msg:
791
+ raise type(e)(f"{error_msg} at index {path_str}") from e
792
+ raise
793
+
794
+ return tuple(results)
795
+
796
+
797
+ def has_async_handlers(child: "ChildNode") -> bool:
798
+ """
799
+ Check if a child node has any async handlers implemented.
800
+
801
+ Args:
802
+ child (ChildNode):
803
+ The child node to check.
804
+
805
+ Returns:
806
+ bool:
807
+ `True` if any handler is async, `False` otherwise.
808
+ """
809
+ for handler_name in ALL_HANDLER_NAMES:
810
+ if _is_handler_implemented(child, handler_name):
811
+ handler = getattr(child, handler_name)
812
+ if asyncio.iscoroutinefunction(handler):
813
+ return True
814
+
815
+ return False
816
+
817
+
818
+ async def dispatch_to_child_async(
819
+ child: "ChildNode",
820
+ value: Any,
821
+ context: "Context",
822
+ ) -> Any:
823
+ """
824
+ Async version of dispatch_to_child for async handler support.
825
+
826
+ Dispatch value to appropriate child handler with priority system.
827
+
828
+ Args:
829
+ child (ChildNode):
830
+ The child node to dispatch to.
831
+ value (Any):
832
+ The value to process.
833
+ context (Context):
834
+ Processing context with parent, siblings, tags.
835
+
836
+ Returns:
837
+ Any:
838
+ The processed value. Returns original value if the
839
+ handler returns `None`.
840
+
841
+ Raises:
842
+ UnhandledTypeError:
843
+ If no handler is implemented for this value type.
844
+ InvalidHandlerError:
845
+ If `handle_tag` returns a modified dictionary.
846
+ """
847
+ if isinstance(value, tuple):
848
+ is_container = context.click_context.meta.get("click_extended", {}).get(
849
+ "is_container_tuple", False
850
+ )
851
+ if is_container:
852
+ return await _process_container_tuple_async(
853
+ child,
854
+ value, # type: ignore
855
+ context,
856
+ )
857
+
858
+ if value is None:
859
+ # Handle None
860
+ if _is_handler_implemented(child, "handle_none"):
861
+ try:
862
+ none_handler = child.handle_none
863
+ if asyncio.iscoroutinefunction(none_handler):
864
+ result = await none_handler(
865
+ context, *child.process_args, **child.process_kwargs
866
+ )
867
+ else:
868
+ result = none_handler(
869
+ context, *child.process_args, **child.process_kwargs
870
+ )
871
+ return value if result is None else result # type: ignore
872
+ except NotImplementedError:
873
+ pass
874
+
875
+ for handler_name in TYPE_SPECIFIC_HANDLERS:
876
+ if _is_handler_implemented(child, handler_name):
877
+ if _should_call_handler(child, handler_name, value):
878
+ try:
879
+ handler = getattr(child, handler_name)
880
+ if asyncio.iscoroutinefunction(handler):
881
+ result = await handler(
882
+ value,
883
+ context,
884
+ *child.process_args,
885
+ **child.process_kwargs,
886
+ )
887
+ else:
888
+ result = handler(
889
+ value,
890
+ context,
891
+ *child.process_args,
892
+ **child.process_kwargs,
893
+ )
894
+
895
+ if result is None:
896
+ return value
897
+ return result
898
+ except NotImplementedError:
899
+ pass
900
+
901
+ # Handle all
902
+ try:
903
+ if _should_call_handler(child, "handle_all", value):
904
+ all_handler = child.handle_all
905
+ if asyncio.iscoroutinefunction(all_handler):
906
+ result = await all_handler(
907
+ value,
908
+ context,
909
+ *child.process_args,
910
+ **child.process_kwargs,
911
+ )
912
+ else:
913
+ result = all_handler(
914
+ value,
915
+ context,
916
+ *child.process_args,
917
+ **child.process_kwargs,
918
+ )
919
+ return value if result is None else result
920
+ except NotImplementedError:
921
+ pass
922
+
923
+ return None
924
+
925
+ handler_name = _determine_handler(
926
+ child,
927
+ value,
928
+ context,
929
+ ) # type: ignore[assignment]
930
+
931
+ # Handle specific
932
+ if handler_name:
933
+ try:
934
+ if _should_call_handler(child, handler_name, value):
935
+ if "click_extended" in context.click_context.meta:
936
+ context.click_context.meta["click_extended"][
937
+ "handler_method"
938
+ ] = handler_name # type: ignore[assignment]
939
+
940
+ handler = getattr(child, handler_name)
941
+ hints = get_type_hints(handler)
942
+
943
+ if "value" in hints:
944
+ is_valid, error_msg = _validate_handler_type(
945
+ handler_name, value, hints["value"]
946
+ )
947
+ if not is_valid:
948
+ raise ProcessError(
949
+ f"Type mismatch in {handler_name}: " f"{error_msg}"
950
+ )
951
+
952
+ if asyncio.iscoroutinefunction(handler):
953
+ result = await handler(
954
+ value,
955
+ context,
956
+ *child.process_args,
957
+ **child.process_kwargs,
958
+ )
959
+ else:
960
+ result = handler(
961
+ value,
962
+ context,
963
+ *child.process_args,
964
+ **child.process_kwargs,
965
+ )
966
+
967
+ if handler_name == "handle_tag" and result is not None:
968
+ message = (
969
+ "Method handle_tag() is validation-only and "
970
+ "does not support transformations."
971
+ )
972
+
973
+ tip = (
974
+ "Remove the return statement to make it "
975
+ "validation-only or move the "
976
+ "transformation logic to the parent node."
977
+ )
978
+
979
+ raise InvalidHandlerError(message=message, tip=tip)
980
+
981
+ return value if result is None else result # type: ignore
982
+ except NotImplementedError:
983
+ pass
984
+
985
+ try:
986
+ if _should_call_handler(child, "handle_all", value):
987
+ if "click_extended" in context.click_context.meta:
988
+ context.click_context.meta["click_extended"][
989
+ "handler_method"
990
+ ] = "handle_all"
991
+
992
+ handler = child.handle_all
993
+ if asyncio.iscoroutinefunction(handler):
994
+ result = await handler(
995
+ value, context, *child.process_args, **child.process_kwargs
996
+ )
997
+ else:
998
+ result = handler(
999
+ value, context, *child.process_args, **child.process_kwargs
1000
+ )
1001
+ return value if result is None else result # type: ignore
1002
+ except NotImplementedError:
1003
+ pass
1004
+
1005
+ raise UnhandledTypeError(
1006
+ child_name=child.name,
1007
+ value_type=type(value).__name__, # type: ignore
1008
+ implemented_handlers=_get_implemented_handlers(child),
1009
+ )
1010
+
1011
+
1012
+ __all__ = [
1013
+ "dispatch_to_child",
1014
+ "dispatch_to_child_async",
1015
+ "has_async_handlers",
1016
+ ]