click-extended 1.0.6__py3-none-any.whl → 1.0.8__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.
@@ -1,7 +1,7 @@
1
1
  """Check if a value matches a regex pattern."""
2
2
 
3
3
  import re
4
- from typing import Any
4
+ from typing import Any, Union
5
5
 
6
6
  from click_extended.core.nodes.child_node import ChildNode
7
7
  from click_extended.core.other.context import Context
@@ -18,17 +18,29 @@ class Regex(ChildNode):
18
18
  *args: Any,
19
19
  **kwargs: Any,
20
20
  ) -> Any:
21
- patterns = kwargs["patterns"]
21
+ patterns: tuple[Union[str, re.Pattern[str]], ...] = kwargs["patterns"]
22
+ flags: int = kwargs.get("flags", 0)
23
+
22
24
  for pattern in patterns:
23
- if re.fullmatch(pattern, value):
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):
24
32
  return value
25
33
 
34
+ pattern_strs: list[str] = [
35
+ p.pattern if isinstance(p, re.Pattern) else p for p in patterns
36
+ ]
26
37
  raise ValueError(
27
- f"Value '{value}' does not match any of the patterns: {patterns}"
38
+ f"Value '{value}' does not match any "
39
+ + f"of the patterns: {pattern_strs}"
28
40
  )
29
41
 
30
42
 
31
- def regex(*patterns: str) -> Decorator:
43
+ def regex(*patterns: Union[str, re.Pattern[str]], flags: int = 0) -> Decorator:
32
44
  """
33
45
  Check if a value matches a regex pattern.
34
46
 
@@ -37,11 +49,17 @@ def regex(*patterns: str) -> Decorator:
37
49
  Supports: `str`
38
50
 
39
51
  Args:
40
- *patterns (str):
41
- The regex patterns to check against.
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).
42
60
 
43
61
  Returns:
44
62
  Decorator:
45
63
  The decorated function.
46
64
  """
47
- return Regex.as_decorator(patterns=patterns)
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
@@ -52,7 +52,7 @@ click_extended/decorators/check/is_url.py,sha256=SLFO4v70CbuxOLr2zPMwH-CU8XqS1fa
52
52
  click_extended/decorators/check/is_uuid.py,sha256=QmCpQ6k7Z6zX0cICySqhKFr5wv0jkbGL7e1AA_jeFLw,844
53
53
  click_extended/decorators/check/length.py,sha256=guenyyVsoXHQcj0iZqqLcPxhujYTL6XKgBmgBmoG_Ls,1930
54
54
  click_extended/decorators/check/not_empty.py,sha256=TdznbvzhYJlWm2SoSxTTcpLT0SGLQMkXx8TTUhIqM5w,1125
55
- click_extended/decorators/check/regex.py,sha256=Vf6Tgx1gzmjKgWIT0nPtaFNv2taTdvVDXOWqmZg4Pm0,1066
55
+ click_extended/decorators/check/regex.py,sha256=J4lQ0_DllckXdJYhUBY65p8_HVl8SoKNJXn2Pc-revw,1945
56
56
  click_extended/decorators/check/requires.py,sha256=t_K4MIuli3oNkeHaNjIIEvwnwDop7bCrtKovL0HHz2c,5644
57
57
  click_extended/decorators/check/starts_with.py,sha256=0XDhJC9_2-BgDeb2ASnHWPhkXYFizHy47ns0M6dRjKg,2561
58
58
  click_extended/decorators/check/truthy.py,sha256=rfmQvHn-AN-y6knopta5PnYYvzgFVOkM1TqxDUI-nfw,748
@@ -97,7 +97,8 @@ click_extended/decorators/math/rounded.py,sha256=bHYZQbtd5zRCd3BOEnjxTGdFAmQAL5l
97
97
  click_extended/decorators/math/sqrt.py,sha256=CWZoQpVw2lbP_LNeg-tWW8fz9VEHTX8eQNPNuDVoTcg,834
98
98
  click_extended/decorators/math/subtract.py,sha256=A7TMByTUH3coSEe7iRN9NkHW2mv47kufb1H1adomG4A,806
99
99
  click_extended/decorators/math/to_percent.py,sha256=39vY13u7Z-pyob1vCoPBDy10FqJasJTVzHcrpTH6ERQ,1374
100
- click_extended/decorators/misc/__init__.py,sha256=TDLrR2mZj_oqAcWEqB_C19Hxn8ClrZW2CbuTmJNRblU,553
100
+ click_extended/decorators/misc/__init__.py,sha256=Cl2u81f_dkvHoMoSb82L1KuNVM-3b1lzKHUMAuxhmH0,662
101
+ click_extended/decorators/misc/catch.py,sha256=Ft8CbTlX6OgeNA8re_epKfwK4HyWQ1eUuCbhNqzpNbw,9343
101
102
  click_extended/decorators/misc/choice.py,sha256=RTVVjl0YUZ2Ti4j6yoHkUpJ0aqNwcnc3rTfmCVr0y0U,4354
102
103
  click_extended/decorators/misc/confirm_if.py,sha256=Rf3HcVVTRqz3sGSiPT02yz65_L351Fzkuw2V6pljlmY,4786
103
104
  click_extended/decorators/misc/default.py,sha256=TjZJKmHEqNThzyD6hOjQy2_QgHRenKILnAwfV1V5KAw,2473
@@ -143,9 +144,9 @@ click_extended/utils/naming.py,sha256=kNmzOqidgZZ1dE5aMYrxpWt4VwcLuEiFunpumpfHuZ
143
144
  click_extended/utils/process.py,sha256=sU3ZCMjBgjKcDnTRLKPdQ_TAjk0AT7Q8SatagD0JyHA,9729
144
145
  click_extended/utils/selection.py,sha256=_OQC88pGPUh29boxmS5ugXEi9jZGqAG180S27PeQaj0,9875
145
146
  click_extended/utils/time.py,sha256=H5m5caIEau_1GHkiYgKL_LcTtVdw2TkFVbkqJu7A9rQ,1067
146
- click_extended-1.0.6.dist-info/licenses/AUTHORS.md,sha256=NkShPinjqtnRDQVRyVnfJuOGM56sejauE3WRoYCcbtw,132
147
- click_extended-1.0.6.dist-info/licenses/LICENSE,sha256=gjO8hzM4mFSBXFikktaXVSgmXGcre91_GPJ-E_yP56E,1075
148
- click_extended-1.0.6.dist-info/METADATA,sha256=e5g0yuAeIk_iB4JP-GqePD-OMwv2MgOQMSkkttGSNRY,10841
149
- click_extended-1.0.6.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
150
- click_extended-1.0.6.dist-info/top_level.txt,sha256=2G3bm6tCNv80okRm773jKTk-_z1ElY-seaozZrn_TxA,15
151
- click_extended-1.0.6.dist-info/RECORD,,
147
+ click_extended-1.0.8.dist-info/licenses/AUTHORS.md,sha256=NkShPinjqtnRDQVRyVnfJuOGM56sejauE3WRoYCcbtw,132
148
+ click_extended-1.0.8.dist-info/licenses/LICENSE,sha256=gjO8hzM4mFSBXFikktaXVSgmXGcre91_GPJ-E_yP56E,1075
149
+ click_extended-1.0.8.dist-info/METADATA,sha256=j42ol1yFZRyvuaiM_g7_-LVNJHFuMX6BDdcog4EjTC8,10841
150
+ click_extended-1.0.8.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
151
+ click_extended-1.0.8.dist-info/top_level.txt,sha256=2G3bm6tCNv80okRm773jKTk-_z1ElY-seaozZrn_TxA,15
152
+ click_extended-1.0.8.dist-info/RECORD,,