click-extended 1.0.7__tar.gz → 1.0.9__tar.gz

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 (158) hide show
  1. {click_extended-1.0.7/click_extended.egg-info → click_extended-1.0.9}/PKG-INFO +1 -1
  2. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/nodes/_root_node.py +27 -1
  3. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/misc/__init__.py +4 -0
  4. click_extended-1.0.9/click_extended/decorators/misc/catch.py +343 -0
  5. {click_extended-1.0.7 → click_extended-1.0.9/click_extended.egg-info}/PKG-INFO +1 -1
  6. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended.egg-info/SOURCES.txt +1 -0
  7. {click_extended-1.0.7 → click_extended-1.0.9}/pyproject.toml +8 -2
  8. {click_extended-1.0.7 → click_extended-1.0.9}/AUTHORS.md +0 -0
  9. {click_extended-1.0.7 → click_extended-1.0.9}/LICENSE +0 -0
  10. {click_extended-1.0.7 → click_extended-1.0.9}/README.md +0 -0
  11. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/__init__.py +0 -0
  12. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/classes.py +0 -0
  13. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/__init__.py +0 -0
  14. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/__init__.py +0 -0
  15. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/argument.py +0 -0
  16. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/command.py +0 -0
  17. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/context.py +0 -0
  18. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/env.py +0 -0
  19. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/group.py +0 -0
  20. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/option.py +0 -0
  21. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/prompt.py +0 -0
  22. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/selection.py +0 -0
  23. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/decorators/tag.py +0 -0
  24. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/nodes/__init__.py +0 -0
  25. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/nodes/argument_node.py +0 -0
  26. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/nodes/child_node.py +0 -0
  27. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/nodes/child_validation_node.py +0 -0
  28. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/nodes/node.py +0 -0
  29. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/nodes/option_node.py +0 -0
  30. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/nodes/parent_node.py +0 -0
  31. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/nodes/validation_node.py +0 -0
  32. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/other/__init__.py +0 -0
  33. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/other/_click_command.py +0 -0
  34. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/other/_click_group.py +0 -0
  35. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/other/_tree.py +0 -0
  36. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/other/context.py +0 -0
  37. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/core/other/get_context.py +0 -0
  38. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/__init__.py +0 -0
  39. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/__init__.py +0 -0
  40. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/conflicts.py +0 -0
  41. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/contains.py +0 -0
  42. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/dependencies.py +0 -0
  43. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/divisible_by.py +0 -0
  44. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/ends_with.py +0 -0
  45. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/exclusive.py +0 -0
  46. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/falsy.py +0 -0
  47. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_email.py +0 -0
  48. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_hex_color.py +0 -0
  49. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_hostname.py +0 -0
  50. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_ipv4.py +0 -0
  51. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_ipv6.py +0 -0
  52. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_json.py +0 -0
  53. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_mac_address.py +0 -0
  54. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_negative.py +0 -0
  55. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_non_zero.py +0 -0
  56. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_port.py +0 -0
  57. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_positive.py +0 -0
  58. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_url.py +0 -0
  59. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/is_uuid.py +0 -0
  60. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/length.py +0 -0
  61. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/not_empty.py +0 -0
  62. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/regex.py +0 -0
  63. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/requires.py +0 -0
  64. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/starts_with.py +0 -0
  65. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/check/truthy.py +0 -0
  66. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/compare/__init__.py +0 -0
  67. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/compare/at_least.py +0 -0
  68. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/compare/at_most.py +0 -0
  69. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/compare/between.py +0 -0
  70. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/compare/greater_than.py +0 -0
  71. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/compare/less_than.py +0 -0
  72. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/__init__.py +0 -0
  73. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_angle.py +0 -0
  74. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_area.py +0 -0
  75. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_bits.py +0 -0
  76. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_distance.py +0 -0
  77. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_energy.py +0 -0
  78. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_power.py +0 -0
  79. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_pressure.py +0 -0
  80. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_speed.py +0 -0
  81. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_temperature.py +0 -0
  82. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_time.py +0 -0
  83. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_volume.py +0 -0
  84. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/convert/convert_weight.py +0 -0
  85. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/load/__init__.py +0 -0
  86. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/load/load_csv.py +0 -0
  87. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/load/load_json.py +0 -0
  88. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/load/load_toml.py +0 -0
  89. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/load/load_yaml.py +0 -0
  90. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/__init__.py +0 -0
  91. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/absolute.py +0 -0
  92. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/add.py +0 -0
  93. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/ceil.py +0 -0
  94. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/clamp.py +0 -0
  95. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/divide.py +0 -0
  96. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/floor.py +0 -0
  97. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/maximum.py +0 -0
  98. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/minimum.py +0 -0
  99. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/modulo.py +0 -0
  100. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/multiply.py +0 -0
  101. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/normalize.py +0 -0
  102. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/power.py +0 -0
  103. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/rounded.py +0 -0
  104. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/sqrt.py +0 -0
  105. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/subtract.py +0 -0
  106. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/math/to_percent.py +0 -0
  107. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/misc/choice.py +0 -0
  108. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/misc/confirm_if.py +0 -0
  109. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/misc/default.py +0 -0
  110. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/misc/deprecated.py +0 -0
  111. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/misc/experimental.py +0 -0
  112. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/misc/now.py +0 -0
  113. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/random/__init__.py +0 -0
  114. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/random/random_bool.py +0 -0
  115. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/random/random_choice.py +0 -0
  116. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/random/random_datetime.py +0 -0
  117. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/random/random_float.py +0 -0
  118. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/random/random_integer.py +0 -0
  119. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/random/random_prime.py +0 -0
  120. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/random/random_string.py +0 -0
  121. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/random/random_uuid.py +0 -0
  122. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/__init__.py +0 -0
  123. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/add_prefix.py +0 -0
  124. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/add_suffix.py +0 -0
  125. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/apply.py +0 -0
  126. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/basename.py +0 -0
  127. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/dirname.py +0 -0
  128. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/expand_vars.py +0 -0
  129. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/remove_prefix.py +0 -0
  130. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/remove_suffix.py +0 -0
  131. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/replace.py +0 -0
  132. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/slugify.py +0 -0
  133. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/split.py +0 -0
  134. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/strip.py +0 -0
  135. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/to_case.py +0 -0
  136. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/to_date.py +0 -0
  137. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/to_datetime.py +0 -0
  138. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/to_path.py +0 -0
  139. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/to_time.py +0 -0
  140. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/to_timestamp.py +0 -0
  141. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/decorators/transform/truncate.py +0 -0
  142. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/errors.py +0 -0
  143. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/types.py +0 -0
  144. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/__init__.py +0 -0
  145. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/casing.py +0 -0
  146. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/checks.py +0 -0
  147. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/dispatch.py +0 -0
  148. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/format.py +0 -0
  149. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/humanize.py +0 -0
  150. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/naming.py +0 -0
  151. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/process.py +0 -0
  152. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/selection.py +0 -0
  153. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended/utils/time.py +0 -0
  154. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended.egg-info/dependency_links.txt +0 -0
  155. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended.egg-info/requires.txt +0 -0
  156. {click_extended-1.0.7 → click_extended-1.0.9}/click_extended.egg-info/top_level.txt +0 -0
  157. {click_extended-1.0.7 → click_extended-1.0.9}/setup.cfg +0 -0
  158. {click_extended-1.0.7 → click_extended-1.0.9}/tests/test_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: click_extended
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Summary: An extension to Click with additional features like automatic async support, aliasing and a modular decorator system.
5
5
  Author-email: Marcus Fredriksson <marcus@marcusfredriksson.com>
6
6
  License: MIT License
@@ -664,7 +664,33 @@ class RootNode(Node):
664
664
  context,
665
665
  )
666
666
 
667
- for validation_node in root.tree.validations:
667
+ catch_nodes = [
668
+ v
669
+ for v in root.tree.validations
670
+ if v.__class__.__name__ == "Catch"
671
+ ]
672
+ other_nodes = [
673
+ v
674
+ for v in root.tree.validations
675
+ if v.__class__.__name__ != "Catch"
676
+ ]
677
+ root.tree.validations = catch_nodes + other_nodes
678
+
679
+ for i, validation_node in enumerate(root.tree.validations):
680
+ if validation_node.__class__.__name__ == "Catch":
681
+ if hasattr(
682
+ validation_node, "remaining_validations"
683
+ ):
684
+ validation_node.remaining_validations = (
685
+ root.tree.validations[i + 1 :]
686
+ )
687
+ validation_node.on_finalize(
688
+ custom_context,
689
+ *validation_node.process_args,
690
+ **validation_node.process_kwargs,
691
+ )
692
+ break
693
+
668
694
  validation_node.on_finalize(
669
695
  custom_context,
670
696
  *validation_node.process_args,
@@ -1,5 +1,8 @@
1
1
  """Initialization file for the `click_extended.decorators.misc` module."""
2
2
 
3
+ # pylint: disable=wrong-import-position
4
+
5
+ from click_extended.decorators.misc.catch import catch
3
6
  from click_extended.decorators.misc.choice import choice
4
7
  from click_extended.decorators.misc.confirm_if import confirm_if
5
8
  from click_extended.decorators.misc.default import default
@@ -8,6 +11,7 @@ from click_extended.decorators.misc.experimental import experimental
8
11
  from click_extended.decorators.misc.now import now
9
12
 
10
13
  __all__ = [
14
+ "catch",
11
15
  "choice",
12
16
  "confirm_if",
13
17
  "default",
@@ -0,0 +1,343 @@
1
+ """
2
+ Validation node to catch and handle exceptions from command/group functions.
3
+ """
4
+
5
+ # pylint: disable=wrong-import-position
6
+ # pylint: disable=wrong-import-order
7
+ # pylint: disable=ungrouped-imports
8
+
9
+ import asyncio
10
+ import inspect
11
+ from functools import wraps
12
+ from typing import Any, Callable
13
+ from weakref import WeakKeyDictionary
14
+
15
+ from click_extended.core.nodes.validation_node import ValidationNode
16
+ from click_extended.core.other._tree import Tree
17
+ from click_extended.core.other.context import Context
18
+ from click_extended.types import Decorator
19
+
20
+ _catch_handlers: WeakKeyDictionary[
21
+ Callable[..., Any],
22
+ list[
23
+ tuple[tuple[type[BaseException], ...], Callable[..., Any] | None, bool]
24
+ ],
25
+ ] = WeakKeyDictionary()
26
+
27
+
28
+ class Catch(ValidationNode):
29
+ """
30
+ Catch and handle exceptions from command/group functions and
31
+ validators.
32
+
33
+ Wraps both the validation phase and function execution in a
34
+ try-except block. When an exception is caught, an optional handler
35
+ is invoked. Without a handler, exceptions are silently suppressed.
36
+
37
+ Catches exceptions from:
38
+ - Validation decorators (e.g., @exclusive, @requires)
39
+ - The command/group function itself
40
+
41
+ Handler signatures supported:
42
+ - `handler()` - No arguments, just execute code
43
+ - `handler(exception)` - Receive the exception object
44
+ - `handler(exception, context)` - Receive exception and Context object
45
+
46
+ Examples:
47
+ ```py
48
+ # Catch validation errors from @exclusive
49
+ @command()
50
+ @exclusive("--stock", "--query")
51
+ @catch(ValueError, handler=lambda e: print(f"Error: {e}"))
52
+ def search(stock, query):
53
+ pass
54
+ ```
55
+
56
+ ```py
57
+ # Catch errors from command function
58
+ @command()
59
+ @catch(ValueError, handler=lambda e: print(f"Error: {e}"))
60
+ def cmd():
61
+ raise ValueError("Count must be positive")
62
+ ```
63
+
64
+ ```py
65
+ # Access context information
66
+ @command()
67
+ @catch(
68
+ ValueError,
69
+ handler=lambda e, ctx: print(f"{ctx.info_name}: {e}"),
70
+ )
71
+ def cmd():
72
+ raise ValueError("Failed!")
73
+ ```
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ name: str,
79
+ process_args: tuple[Any, ...] | None = None,
80
+ process_kwargs: dict[str, Any] | None = None,
81
+ **kwargs: Any,
82
+ ) -> None:
83
+ """Initialize Catch validation node with function to wrap."""
84
+ super().__init__(name, process_args, process_kwargs, **kwargs)
85
+ self.wrapped_func: Callable[..., Any] | None = None
86
+ self.remaining_validations: list[ValidationNode] = []
87
+
88
+ def on_finalize(self, context: Context, *args: Any, **kwargs: Any) -> None:
89
+ """
90
+ Execute remaining validations wrapped in exception handling.
91
+
92
+ This catches exceptions from all validators that run after @catch,
93
+ allowing it to catch validation errors like those from @exclusive.
94
+
95
+ Args:
96
+ context: The execution context
97
+ *args: Contains exception types tuple at index 0
98
+ **kwargs: Contains handler, reraise parameters
99
+ """
100
+ exception_types: tuple[type[BaseException], ...] = args[0]
101
+ handler: Callable[..., Any] | None = kwargs.get("handler")
102
+ reraise: bool = kwargs.get("reraise", False)
103
+
104
+ for validation_node in self.remaining_validations:
105
+ try:
106
+ if asyncio.iscoroutinefunction(validation_node.on_finalize):
107
+ asyncio.run(
108
+ validation_node.on_finalize(
109
+ context,
110
+ *validation_node.process_args,
111
+ **validation_node.process_kwargs,
112
+ )
113
+ )
114
+ else:
115
+ validation_node.on_finalize(
116
+ context,
117
+ *validation_node.process_args,
118
+ **validation_node.process_kwargs,
119
+ )
120
+ except BaseException as exc:
121
+ if isinstance(exc, exception_types):
122
+ if handler is not None:
123
+ if asyncio.iscoroutinefunction(handler):
124
+ asyncio.run(_call_handler_async(handler, exc))
125
+ else:
126
+ _call_handler_sync(handler, exc)
127
+
128
+ if not reraise:
129
+ return
130
+
131
+ raise
132
+
133
+
134
+ def catch(
135
+ *exception_types: type[BaseException],
136
+ handler: Callable[..., Any] | None = None,
137
+ reraise: bool = False,
138
+ ) -> Decorator:
139
+ """
140
+ Catch and handle exceptions from command/group functions.
141
+
142
+ Wraps the function in a try-except block. When exceptions occur, an optional
143
+ handler is invoked. If no exception types are specified, catches Exception.
144
+
145
+ Type: `ValidationNode`
146
+
147
+ Args:
148
+ *exception_types: Exception types to catch (defaults to Exception)
149
+ handler: Optional function with signature `()`, `(exception)`, or
150
+ `(exception, context)` to handle caught exceptions
151
+ reraise: If True, re-raise after handling (default: False)
152
+
153
+ Returns:
154
+ Decorator function
155
+
156
+ Raises:
157
+ TypeError: If exception_types contains non-exception classes
158
+
159
+ Examples:
160
+ ```python
161
+ # Simple notification (no arguments)
162
+ @command()
163
+ @catch(ValueError, handler=lambda: print("Error occurred!"))
164
+ def cmd():
165
+ raise ValueError("Invalid input")
166
+ ```
167
+
168
+ ```python
169
+ # Log exception details (exception argument)
170
+ @command()
171
+ @catch(ValueError, handler=lambda e: print(f"Error: {e}"))
172
+ def cmd():
173
+ raise ValueError("Count must be positive")
174
+ ```
175
+
176
+ ```python
177
+ # Access context (exception + context arguments)
178
+ @command()
179
+ @catch(
180
+ ValueError,
181
+ handler=lambda e, ctx: print(f"{ctx.info_name}: {e}"),
182
+ )
183
+ def cmd():
184
+ raise ValueError("Failed!")
185
+ ```
186
+
187
+ ```python
188
+ # Catch multiple exception types
189
+ @command()
190
+ @catch(ValueError, TypeError, handler=lambda e: log_error(e))
191
+ def cmd():
192
+ raise ValueError("Something went wrong")
193
+ ```
194
+
195
+ ```python
196
+ # Silent suppression (no handler)
197
+ @command()
198
+ @catch(RuntimeError)
199
+ def cmd():
200
+ raise RuntimeError("Silently suppressed")
201
+ ```
202
+
203
+ ```python
204
+ # Log then re-raise
205
+ @command()
206
+ @catch(
207
+ ValueError,
208
+ handler=lambda e: print(f"Logging: {e}"),
209
+ reraise=True,
210
+ )
211
+ def cmd():
212
+ raise ValueError("This will be logged and re-raised")
213
+ ```
214
+ """
215
+ if not exception_types:
216
+ exception_types = (Exception,)
217
+
218
+ for exc_type in exception_types:
219
+ if not isinstance(exc_type, type) or not issubclass(
220
+ exc_type, BaseException
221
+ ):
222
+ raise TypeError(
223
+ f"catch() requires exception types, got {exc_type!r}"
224
+ )
225
+
226
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
227
+ """The actual decorator that wraps the function."""
228
+
229
+ instance = Catch(
230
+ name="catch",
231
+ process_args=(exception_types,),
232
+ process_kwargs={"handler": handler, "reraise": reraise},
233
+ )
234
+
235
+ Tree.queue_validation(instance)
236
+
237
+ original_func = func
238
+ while hasattr(original_func, "__wrapped__"):
239
+ original_func = original_func.__wrapped__ # type: ignore
240
+
241
+ if original_func not in _catch_handlers:
242
+ _catch_handlers[original_func] = []
243
+ _catch_handlers[original_func].insert(
244
+ 0, (exception_types, handler, reraise)
245
+ )
246
+
247
+ # Only wrap if this is the first @catch (check handlers dict)
248
+ if len(_catch_handlers[original_func]) > 1:
249
+ return func
250
+
251
+ if asyncio.iscoroutinefunction(func):
252
+
253
+ @wraps(func)
254
+ async def async_wrapper(*call_args: Any, **call_kwargs: Any) -> Any:
255
+ """Async wrapper that catches exceptions."""
256
+ try:
257
+ return await func(*call_args, **call_kwargs)
258
+ except BaseException as exc:
259
+ for exc_types, hdlr, reraise_flag in _catch_handlers.get(
260
+ original_func, []
261
+ ):
262
+ if isinstance(exc, exc_types):
263
+ if hdlr is not None:
264
+ if asyncio.iscoroutinefunction(hdlr):
265
+ await _call_handler_async(hdlr, exc)
266
+ else:
267
+ _call_handler_sync(hdlr, exc)
268
+
269
+ if reraise_flag:
270
+ raise
271
+
272
+ return None
273
+ raise
274
+
275
+ return async_wrapper
276
+
277
+ @wraps(func)
278
+ def sync_wrapper(*call_args: Any, **call_kwargs: Any) -> Any:
279
+ """Sync wrapper that catches exceptions."""
280
+ try:
281
+ return func(*call_args, **call_kwargs)
282
+ except BaseException as exc:
283
+ for exc_types, hdlr, reraise_flag in _catch_handlers.get(
284
+ original_func, []
285
+ ):
286
+ if isinstance(exc, exc_types):
287
+ if hdlr is not None:
288
+ if asyncio.iscoroutinefunction(hdlr):
289
+ asyncio.run(_call_handler_async(hdlr, exc))
290
+ else:
291
+ _call_handler_sync(hdlr, exc)
292
+
293
+ if reraise_flag:
294
+ raise
295
+
296
+ return None
297
+ raise
298
+
299
+ return sync_wrapper
300
+
301
+ return decorator
302
+
303
+
304
+ async def _call_handler_async(
305
+ handler: Callable[..., Any], exc: BaseException
306
+ ) -> Any:
307
+ """Call async handler with appropriate number of arguments."""
308
+ sig = inspect.signature(handler)
309
+ params = list(sig.parameters.values())
310
+
311
+ if len(params) == 0:
312
+ return await handler()
313
+ if len(params) == 1:
314
+ return await handler(exc)
315
+
316
+ import click
317
+
318
+ try:
319
+ ctx = click.get_current_context()
320
+ custom_context = ctx.meta.get("click_extended", {}).get("context")
321
+ return await handler(exc, custom_context)
322
+ except RuntimeError:
323
+ return await handler(exc, None)
324
+
325
+
326
+ def _call_handler_sync(handler: Callable[..., Any], exc: BaseException) -> Any:
327
+ """Call sync handler with appropriate number of arguments."""
328
+ sig = inspect.signature(handler)
329
+ params = list(sig.parameters.values())
330
+
331
+ if len(params) == 0:
332
+ return handler()
333
+ if len(params) == 1:
334
+ return handler(exc)
335
+
336
+ import click
337
+
338
+ try:
339
+ ctx = click.get_current_context()
340
+ custom_context = ctx.meta.get("click_extended", {}).get("context")
341
+ return handler(exc, custom_context)
342
+ except RuntimeError:
343
+ return handler(exc, None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: click_extended
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Summary: An extension to Click with additional features like automatic async support, aliasing and a modular decorator system.
5
5
  Author-email: Marcus Fredriksson <marcus@marcusfredriksson.com>
6
6
  License: MIT License
@@ -107,6 +107,7 @@ click_extended/decorators/math/sqrt.py
107
107
  click_extended/decorators/math/subtract.py
108
108
  click_extended/decorators/math/to_percent.py
109
109
  click_extended/decorators/misc/__init__.py
110
+ click_extended/decorators/misc/catch.py
110
111
  click_extended/decorators/misc/choice.py
111
112
  click_extended/decorators/misc/confirm_if.py
112
113
  click_extended/decorators/misc/default.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "click_extended"
3
- version = "1.0.7"
3
+ version = "1.0.9"
4
4
  description = "An extension to Click with additional features like automatic async support, aliasing and a modular decorator system."
5
5
  authors = [
6
6
  { name = "Marcus Fredriksson", email = "marcus@marcusfredriksson.com" },
@@ -85,7 +85,13 @@ ignore = [
85
85
  ".mypy_cache",
86
86
  "*.egg-info",
87
87
  ]
88
- ignore-paths = ["^tests/.*$", "^docs/.*$", "^\\..*$"]
88
+ ignore-paths = [
89
+ "^tests/.*$",
90
+ "^docs/.*$",
91
+ "^\\..*$",
92
+ "^click_extended/core/nodes/_root_node\\.py$",
93
+ "^click_extended/utils/dispatch\\.py$",
94
+ ]
89
95
  persistent = true
90
96
 
91
97
  [tool.pylint.messages_control]
File without changes
File without changes
File without changes