click-extended 1.0.7__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.
- click_extended/decorators/misc/__init__.py +4 -0
- click_extended/decorators/misc/catch.py +300 -0
- {click_extended-1.0.7.dist-info → click_extended-1.0.8.dist-info}/METADATA +1 -1
- {click_extended-1.0.7.dist-info → click_extended-1.0.8.dist-info}/RECORD +8 -7
- {click_extended-1.0.7.dist-info → click_extended-1.0.8.dist-info}/WHEEL +0 -0
- {click_extended-1.0.7.dist-info → click_extended-1.0.8.dist-info}/licenses/AUTHORS.md +0 -0
- {click_extended-1.0.7.dist-info → click_extended-1.0.8.dist-info}/licenses/LICENSE +0 -0
- {click_extended-1.0.7.dist-info → click_extended-1.0.8.dist-info}/top_level.txt +0 -0
|
@@ -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.
|
|
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
|
|
@@ -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=
|
|
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.
|
|
147
|
-
click_extended-1.0.
|
|
148
|
-
click_extended-1.0.
|
|
149
|
-
click_extended-1.0.
|
|
150
|
-
click_extended-1.0.
|
|
151
|
-
click_extended-1.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|