click-extended 1.0.6__tar.gz → 1.0.8__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 (159) hide show
  1. {click_extended-1.0.6/click_extended.egg-info → click_extended-1.0.8}/PKG-INFO +1 -1
  2. click_extended-1.0.8/click_extended/decorators/check/regex.py +65 -0
  3. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/misc/__init__.py +4 -0
  4. click_extended-1.0.8/click_extended/decorators/misc/catch.py +300 -0
  5. {click_extended-1.0.6 → click_extended-1.0.8/click_extended.egg-info}/PKG-INFO +1 -1
  6. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended.egg-info/SOURCES.txt +1 -0
  7. {click_extended-1.0.6 → click_extended-1.0.8}/pyproject.toml +8 -2
  8. click_extended-1.0.6/click_extended/decorators/check/regex.py +0 -47
  9. {click_extended-1.0.6 → click_extended-1.0.8}/AUTHORS.md +0 -0
  10. {click_extended-1.0.6 → click_extended-1.0.8}/LICENSE +0 -0
  11. {click_extended-1.0.6 → click_extended-1.0.8}/README.md +0 -0
  12. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/__init__.py +0 -0
  13. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/classes.py +0 -0
  14. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/__init__.py +0 -0
  15. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/__init__.py +0 -0
  16. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/argument.py +0 -0
  17. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/command.py +0 -0
  18. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/context.py +0 -0
  19. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/env.py +0 -0
  20. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/group.py +0 -0
  21. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/option.py +0 -0
  22. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/prompt.py +0 -0
  23. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/selection.py +0 -0
  24. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/decorators/tag.py +0 -0
  25. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/nodes/__init__.py +0 -0
  26. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/nodes/_root_node.py +0 -0
  27. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/nodes/argument_node.py +0 -0
  28. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/nodes/child_node.py +0 -0
  29. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/nodes/child_validation_node.py +0 -0
  30. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/nodes/node.py +0 -0
  31. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/nodes/option_node.py +0 -0
  32. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/nodes/parent_node.py +0 -0
  33. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/nodes/validation_node.py +0 -0
  34. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/other/__init__.py +0 -0
  35. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/other/_click_command.py +0 -0
  36. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/other/_click_group.py +0 -0
  37. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/other/_tree.py +0 -0
  38. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/other/context.py +0 -0
  39. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/core/other/get_context.py +0 -0
  40. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/__init__.py +0 -0
  41. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/__init__.py +0 -0
  42. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/conflicts.py +0 -0
  43. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/contains.py +0 -0
  44. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/dependencies.py +0 -0
  45. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/divisible_by.py +0 -0
  46. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/ends_with.py +0 -0
  47. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/exclusive.py +0 -0
  48. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/falsy.py +0 -0
  49. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_email.py +0 -0
  50. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_hex_color.py +0 -0
  51. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_hostname.py +0 -0
  52. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_ipv4.py +0 -0
  53. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_ipv6.py +0 -0
  54. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_json.py +0 -0
  55. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_mac_address.py +0 -0
  56. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_negative.py +0 -0
  57. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_non_zero.py +0 -0
  58. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_port.py +0 -0
  59. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_positive.py +0 -0
  60. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_url.py +0 -0
  61. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/is_uuid.py +0 -0
  62. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/length.py +0 -0
  63. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/not_empty.py +0 -0
  64. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/requires.py +0 -0
  65. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/starts_with.py +0 -0
  66. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/check/truthy.py +0 -0
  67. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/compare/__init__.py +0 -0
  68. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/compare/at_least.py +0 -0
  69. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/compare/at_most.py +0 -0
  70. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/compare/between.py +0 -0
  71. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/compare/greater_than.py +0 -0
  72. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/compare/less_than.py +0 -0
  73. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/__init__.py +0 -0
  74. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_angle.py +0 -0
  75. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_area.py +0 -0
  76. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_bits.py +0 -0
  77. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_distance.py +0 -0
  78. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_energy.py +0 -0
  79. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_power.py +0 -0
  80. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_pressure.py +0 -0
  81. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_speed.py +0 -0
  82. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_temperature.py +0 -0
  83. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_time.py +0 -0
  84. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_volume.py +0 -0
  85. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/convert/convert_weight.py +0 -0
  86. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/load/__init__.py +0 -0
  87. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/load/load_csv.py +0 -0
  88. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/load/load_json.py +0 -0
  89. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/load/load_toml.py +0 -0
  90. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/load/load_yaml.py +0 -0
  91. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/__init__.py +0 -0
  92. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/absolute.py +0 -0
  93. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/add.py +0 -0
  94. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/ceil.py +0 -0
  95. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/clamp.py +0 -0
  96. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/divide.py +0 -0
  97. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/floor.py +0 -0
  98. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/maximum.py +0 -0
  99. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/minimum.py +0 -0
  100. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/modulo.py +0 -0
  101. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/multiply.py +0 -0
  102. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/normalize.py +0 -0
  103. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/power.py +0 -0
  104. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/rounded.py +0 -0
  105. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/sqrt.py +0 -0
  106. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/subtract.py +0 -0
  107. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/math/to_percent.py +0 -0
  108. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/misc/choice.py +0 -0
  109. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/misc/confirm_if.py +0 -0
  110. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/misc/default.py +0 -0
  111. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/misc/deprecated.py +0 -0
  112. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/misc/experimental.py +0 -0
  113. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/misc/now.py +0 -0
  114. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/random/__init__.py +0 -0
  115. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/random/random_bool.py +0 -0
  116. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/random/random_choice.py +0 -0
  117. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/random/random_datetime.py +0 -0
  118. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/random/random_float.py +0 -0
  119. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/random/random_integer.py +0 -0
  120. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/random/random_prime.py +0 -0
  121. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/random/random_string.py +0 -0
  122. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/random/random_uuid.py +0 -0
  123. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/__init__.py +0 -0
  124. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/add_prefix.py +0 -0
  125. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/add_suffix.py +0 -0
  126. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/apply.py +0 -0
  127. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/basename.py +0 -0
  128. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/dirname.py +0 -0
  129. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/expand_vars.py +0 -0
  130. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/remove_prefix.py +0 -0
  131. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/remove_suffix.py +0 -0
  132. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/replace.py +0 -0
  133. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/slugify.py +0 -0
  134. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/split.py +0 -0
  135. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/strip.py +0 -0
  136. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/to_case.py +0 -0
  137. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/to_date.py +0 -0
  138. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/to_datetime.py +0 -0
  139. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/to_path.py +0 -0
  140. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/to_time.py +0 -0
  141. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/to_timestamp.py +0 -0
  142. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/decorators/transform/truncate.py +0 -0
  143. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/errors.py +0 -0
  144. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/types.py +0 -0
  145. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/__init__.py +0 -0
  146. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/casing.py +0 -0
  147. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/checks.py +0 -0
  148. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/dispatch.py +0 -0
  149. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/format.py +0 -0
  150. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/humanize.py +0 -0
  151. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/naming.py +0 -0
  152. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/process.py +0 -0
  153. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/selection.py +0 -0
  154. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended/utils/time.py +0 -0
  155. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended.egg-info/dependency_links.txt +0 -0
  156. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended.egg-info/requires.txt +0 -0
  157. {click_extended-1.0.6 → click_extended-1.0.8}/click_extended.egg-info/top_level.txt +0 -0
  158. {click_extended-1.0.6 → click_extended-1.0.8}/setup.cfg +0 -0
  159. {click_extended-1.0.6 → click_extended-1.0.8}/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.6
3
+ Version: 1.0.8
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
@@ -0,0 +1,65 @@
1
+ """Check if a value matches a regex pattern."""
2
+
3
+ import re
4
+ from typing import Any, Union
5
+
6
+ from click_extended.core.nodes.child_node import ChildNode
7
+ from click_extended.core.other.context import Context
8
+ from click_extended.types import Decorator
9
+
10
+
11
+ class Regex(ChildNode):
12
+ """Check if a value matches a regex pattern."""
13
+
14
+ def handle_str(
15
+ self,
16
+ value: str,
17
+ context: Context,
18
+ *args: Any,
19
+ **kwargs: Any,
20
+ ) -> Any:
21
+ patterns: tuple[Union[str, re.Pattern[str]], ...] = kwargs["patterns"]
22
+ flags: int = kwargs.get("flags", 0)
23
+
24
+ for pattern in patterns:
25
+ compiled_pattern: re.Pattern[str]
26
+ if isinstance(pattern, re.Pattern):
27
+ compiled_pattern = pattern
28
+ else:
29
+ compiled_pattern = re.compile(pattern, flags)
30
+
31
+ if compiled_pattern.fullmatch(value):
32
+ return value
33
+
34
+ pattern_strs: list[str] = [
35
+ p.pattern if isinstance(p, re.Pattern) else p for p in patterns
36
+ ]
37
+ raise ValueError(
38
+ f"Value '{value}' does not match any "
39
+ + f"of the patterns: {pattern_strs}"
40
+ )
41
+
42
+
43
+ def regex(*patterns: Union[str, re.Pattern[str]], flags: int = 0) -> Decorator:
44
+ """
45
+ Check if a value matches a regex pattern.
46
+
47
+ Type: `ChildNode`
48
+
49
+ Supports: `str`
50
+
51
+ Args:
52
+ *patterns (Union[str, Pattern[str]]):
53
+ The regex patterns to check against. Can be strings or compiled
54
+ Pattern objects. If strings are provided, they will be compiled
55
+ with the specified flags.
56
+ flags (int):
57
+ Regex flags to use when compiling string patterns (e.g.,
58
+ re.IGNORECASE, re.MULTILINE). Ignored for pre-compiled patterns.
59
+ Default is 0 (no flags).
60
+
61
+ Returns:
62
+ Decorator:
63
+ The decorated function.
64
+ """
65
+ return Regex.as_decorator(patterns=patterns, flags=flags)
@@ -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,300 @@
1
+ """
2
+ Validation node to catch and handle exceptions from command/group functions.
3
+ """
4
+
5
+ import asyncio
6
+ import inspect
7
+ from functools import wraps
8
+ from typing import Any, Callable
9
+ from weakref import WeakKeyDictionary
10
+
11
+ from click_extended.core.nodes.validation_node import ValidationNode
12
+ from click_extended.core.other._tree import Tree
13
+ from click_extended.core.other.context import Context
14
+ from click_extended.types import Decorator
15
+
16
+ _catch_handlers: WeakKeyDictionary[
17
+ Callable[..., Any],
18
+ list[
19
+ tuple[tuple[type[BaseException], ...], Callable[..., Any] | None, bool]
20
+ ],
21
+ ] = WeakKeyDictionary()
22
+
23
+
24
+ class Catch(ValidationNode):
25
+ """
26
+ Catch and handle exceptions from command/group functions.
27
+
28
+ Wraps the decorated function in a try-except block. When an exception
29
+ is caught, an optional handler is invoked. Without a handler, exceptions
30
+ are silently suppressed.
31
+
32
+ Handler signatures supported:
33
+ - `handler()` - No arguments, just execute code
34
+ - `handler(exception)` - Receive the exception object
35
+ - `handler(exception, context)` - Receive exception and Context object
36
+
37
+ Examples:
38
+ ```py
39
+ # Simple error logging
40
+ @command()
41
+ @catch(ValueError, handler=lambda: print("Invalid value!"))
42
+ def cmd():
43
+ raise ValueError("Bad input")
44
+ ```
45
+
46
+ ```py
47
+ # Handle exception with details
48
+ @command()
49
+ @catch(ValueError, handler=lambda e: print(f"Error: {e}"))
50
+ def cmd():
51
+ raise ValueError("Count must be positive")
52
+ ```
53
+
54
+ ```py
55
+ # Access context information
56
+ @command()
57
+ @catch(
58
+ ValueError,
59
+ handler=lambda e, ctx: print(f"{ctx.info_name}: {e}"),
60
+ )
61
+ def cmd():
62
+ raise ValueError("Failed!")
63
+ ```
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ name: str,
69
+ process_args: tuple[Any, ...] | None = None,
70
+ process_kwargs: dict[str, Any] | None = None,
71
+ **kwargs: Any,
72
+ ) -> None:
73
+ """Initialize Catch validation node with function to wrap."""
74
+ super().__init__(name, process_args, process_kwargs, **kwargs)
75
+ self.wrapped_func: Callable[..., Any] | None = None
76
+
77
+ def on_finalize(self, context: Context, *args: Any, **kwargs: Any) -> None:
78
+ """
79
+ Store exception handler configuration for later use.
80
+
81
+ The actual exception catching happens when the function is invoked,
82
+ which is done by wrapping the function in the decorator.
83
+
84
+ Args:
85
+ context: The execution context
86
+ *args: Contains exception types tuple at index 0
87
+ **kwargs: Contains handler, reraise parameters
88
+ """
89
+
90
+
91
+ def catch(
92
+ *exception_types: type[BaseException],
93
+ handler: Callable[..., Any] | None = None,
94
+ reraise: bool = False,
95
+ ) -> Decorator:
96
+ """
97
+ Catch and handle exceptions from command/group functions.
98
+
99
+ Wraps the function in a try-except block. When exceptions occur, an optional
100
+ handler is invoked. If no exception types are specified, catches Exception.
101
+
102
+ Type: `ValidationNode`
103
+
104
+ Args:
105
+ *exception_types: Exception types to catch (defaults to Exception)
106
+ handler: Optional function with signature `()`, `(exception)`, or
107
+ `(exception, context)` to handle caught exceptions
108
+ reraise: If True, re-raise after handling (default: False)
109
+
110
+ Returns:
111
+ Decorator function
112
+
113
+ Raises:
114
+ TypeError: If exception_types contains non-exception classes
115
+
116
+ Examples:
117
+ ```python
118
+ # Simple notification (no arguments)
119
+ @command()
120
+ @catch(ValueError, handler=lambda: print("Error occurred!"))
121
+ def cmd():
122
+ raise ValueError("Invalid input")
123
+ ```
124
+
125
+ ```python
126
+ # Log exception details (exception argument)
127
+ @command()
128
+ @catch(ValueError, handler=lambda e: print(f"Error: {e}"))
129
+ def cmd():
130
+ raise ValueError("Count must be positive")
131
+ ```
132
+
133
+ ```python
134
+ # Access context (exception + context arguments)
135
+ @command()
136
+ @catch(
137
+ ValueError,
138
+ handler=lambda e, ctx: print(f"{ctx.info_name}: {e}"),
139
+ )
140
+ def cmd():
141
+ raise ValueError("Failed!")
142
+ ```
143
+
144
+ ```python
145
+ # Catch multiple exception types
146
+ @command()
147
+ @catch(ValueError, TypeError, handler=lambda e: log_error(e))
148
+ def cmd():
149
+ raise ValueError("Something went wrong")
150
+ ```
151
+
152
+ ```python
153
+ # Silent suppression (no handler)
154
+ @command()
155
+ @catch(RuntimeError)
156
+ def cmd():
157
+ raise RuntimeError("Silently suppressed")
158
+ ```
159
+
160
+ ```python
161
+ # Log then re-raise
162
+ @command()
163
+ @catch(
164
+ ValueError,
165
+ handler=lambda e: print(f"Logging: {e}"),
166
+ reraise=True,
167
+ )
168
+ def cmd():
169
+ raise ValueError("This will be logged and re-raised")
170
+ ```
171
+ """
172
+ if not exception_types:
173
+ exception_types = (Exception,)
174
+
175
+ for exc_type in exception_types:
176
+ if not isinstance(exc_type, type) or not issubclass(
177
+ exc_type, BaseException
178
+ ):
179
+ raise TypeError(
180
+ f"catch() requires exception types, got {exc_type!r}"
181
+ )
182
+
183
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
184
+ """The actual decorator that wraps the function."""
185
+
186
+ instance = Catch(
187
+ name="catch",
188
+ process_args=(exception_types,),
189
+ process_kwargs={"handler": handler, "reraise": reraise},
190
+ )
191
+
192
+ Tree.queue_validation(instance)
193
+
194
+ original_func = func
195
+ while hasattr(original_func, "__wrapped__"):
196
+ original_func = original_func.__wrapped__ # type: ignore
197
+
198
+ if original_func not in _catch_handlers:
199
+ _catch_handlers[original_func] = []
200
+ _catch_handlers[original_func].insert(
201
+ 0, (exception_types, handler, reraise)
202
+ )
203
+
204
+ # Only wrap if this is the first @catch (check handlers dict)
205
+ if len(_catch_handlers[original_func]) > 1:
206
+ return func
207
+
208
+ if asyncio.iscoroutinefunction(func):
209
+
210
+ @wraps(func)
211
+ async def async_wrapper(*call_args: Any, **call_kwargs: Any) -> Any:
212
+ """Async wrapper that catches exceptions."""
213
+ try:
214
+ return await func(*call_args, **call_kwargs)
215
+ except BaseException as exc:
216
+ for exc_types, hdlr, reraise_flag in _catch_handlers.get(
217
+ original_func, []
218
+ ):
219
+ if isinstance(exc, exc_types):
220
+ if hdlr is not None:
221
+ if asyncio.iscoroutinefunction(hdlr):
222
+ await _call_handler_async(hdlr, exc)
223
+ else:
224
+ _call_handler_sync(hdlr, exc)
225
+
226
+ if reraise_flag:
227
+ raise
228
+
229
+ return None
230
+ raise
231
+
232
+ return async_wrapper
233
+
234
+ @wraps(func)
235
+ def sync_wrapper(*call_args: Any, **call_kwargs: Any) -> Any:
236
+ """Sync wrapper that catches exceptions."""
237
+ try:
238
+ return func(*call_args, **call_kwargs)
239
+ except BaseException as exc:
240
+ for exc_types, hdlr, reraise_flag in _catch_handlers.get(
241
+ original_func, []
242
+ ):
243
+ if isinstance(exc, exc_types):
244
+ if hdlr is not None:
245
+ if asyncio.iscoroutinefunction(hdlr):
246
+ asyncio.run(_call_handler_async(hdlr, exc))
247
+ else:
248
+ _call_handler_sync(hdlr, exc)
249
+
250
+ if reraise_flag:
251
+ raise
252
+
253
+ return None
254
+ raise
255
+
256
+ return sync_wrapper
257
+
258
+ return decorator
259
+
260
+
261
+ async def _call_handler_async(
262
+ handler: Callable[..., Any], exc: BaseException
263
+ ) -> Any:
264
+ """Call async handler with appropriate number of arguments."""
265
+ sig = inspect.signature(handler)
266
+ params = list(sig.parameters.values())
267
+
268
+ if len(params) == 0:
269
+ return await handler()
270
+ if len(params) == 1:
271
+ return await handler(exc)
272
+
273
+ import click
274
+
275
+ try:
276
+ ctx = click.get_current_context()
277
+ custom_context = ctx.meta.get("click_extended", {}).get("context")
278
+ return await handler(exc, custom_context)
279
+ except RuntimeError:
280
+ return await handler(exc, None)
281
+
282
+
283
+ def _call_handler_sync(handler: Callable[..., Any], exc: BaseException) -> Any:
284
+ """Call sync handler with appropriate number of arguments."""
285
+ sig = inspect.signature(handler)
286
+ params = list(sig.parameters.values())
287
+
288
+ if len(params) == 0:
289
+ return handler()
290
+ if len(params) == 1:
291
+ return handler(exc)
292
+
293
+ import click
294
+
295
+ try:
296
+ ctx = click.get_current_context()
297
+ custom_context = ctx.meta.get("click_extended", {}).get("context")
298
+ return handler(exc, custom_context)
299
+ except RuntimeError:
300
+ return handler(exc, None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: click_extended
3
- Version: 1.0.6
3
+ Version: 1.0.8
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.6"
3
+ version = "1.0.8"
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]
@@ -1,47 +0,0 @@
1
- """Check if a value matches a regex pattern."""
2
-
3
- import re
4
- from typing import Any
5
-
6
- from click_extended.core.nodes.child_node import ChildNode
7
- from click_extended.core.other.context import Context
8
- from click_extended.types import Decorator
9
-
10
-
11
- class Regex(ChildNode):
12
- """Check if a value matches a regex pattern."""
13
-
14
- def handle_str(
15
- self,
16
- value: str,
17
- context: Context,
18
- *args: Any,
19
- **kwargs: Any,
20
- ) -> Any:
21
- patterns = kwargs["patterns"]
22
- for pattern in patterns:
23
- if re.fullmatch(pattern, value):
24
- return value
25
-
26
- raise ValueError(
27
- f"Value '{value}' does not match any of the patterns: {patterns}"
28
- )
29
-
30
-
31
- def regex(*patterns: str) -> Decorator:
32
- """
33
- Check if a value matches a regex pattern.
34
-
35
- Type: `ChildNode`
36
-
37
- Supports: `str`
38
-
39
- Args:
40
- *patterns (str):
41
- The regex patterns to check against.
42
-
43
- Returns:
44
- Decorator:
45
- The decorated function.
46
- """
47
- return Regex.as_decorator(patterns=patterns)
File without changes
File without changes
File without changes