stouputils 1.14.0__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.
- stouputils/__init__.py +40 -0
- stouputils/__main__.py +86 -0
- stouputils/_deprecated.py +37 -0
- stouputils/all_doctests.py +160 -0
- stouputils/applications/__init__.py +22 -0
- stouputils/applications/automatic_docs.py +634 -0
- stouputils/applications/upscaler/__init__.py +39 -0
- stouputils/applications/upscaler/config.py +128 -0
- stouputils/applications/upscaler/image.py +247 -0
- stouputils/applications/upscaler/video.py +287 -0
- stouputils/archive.py +344 -0
- stouputils/backup.py +488 -0
- stouputils/collections.py +244 -0
- stouputils/continuous_delivery/__init__.py +27 -0
- stouputils/continuous_delivery/cd_utils.py +243 -0
- stouputils/continuous_delivery/github.py +522 -0
- stouputils/continuous_delivery/pypi.py +130 -0
- stouputils/continuous_delivery/pyproject.py +147 -0
- stouputils/continuous_delivery/stubs.py +86 -0
- stouputils/ctx.py +408 -0
- stouputils/data_science/config/get.py +51 -0
- stouputils/data_science/config/set.py +125 -0
- stouputils/data_science/data_processing/image/__init__.py +66 -0
- stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
- stouputils/data_science/data_processing/image/axis_flip.py +58 -0
- stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
- stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
- stouputils/data_science/data_processing/image/blur.py +59 -0
- stouputils/data_science/data_processing/image/brightness.py +54 -0
- stouputils/data_science/data_processing/image/canny.py +110 -0
- stouputils/data_science/data_processing/image/clahe.py +92 -0
- stouputils/data_science/data_processing/image/common.py +30 -0
- stouputils/data_science/data_processing/image/contrast.py +53 -0
- stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
- stouputils/data_science/data_processing/image/denoise.py +378 -0
- stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
- stouputils/data_science/data_processing/image/invert.py +64 -0
- stouputils/data_science/data_processing/image/laplacian.py +60 -0
- stouputils/data_science/data_processing/image/median_blur.py +52 -0
- stouputils/data_science/data_processing/image/noise.py +59 -0
- stouputils/data_science/data_processing/image/normalize.py +65 -0
- stouputils/data_science/data_processing/image/random_erase.py +66 -0
- stouputils/data_science/data_processing/image/resize.py +69 -0
- stouputils/data_science/data_processing/image/rotation.py +80 -0
- stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
- stouputils/data_science/data_processing/image/sharpening.py +55 -0
- stouputils/data_science/data_processing/image/shearing.py +64 -0
- stouputils/data_science/data_processing/image/threshold.py +64 -0
- stouputils/data_science/data_processing/image/translation.py +71 -0
- stouputils/data_science/data_processing/image/zoom.py +83 -0
- stouputils/data_science/data_processing/image_augmentation.py +118 -0
- stouputils/data_science/data_processing/image_preprocess.py +183 -0
- stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
- stouputils/data_science/data_processing/technique.py +481 -0
- stouputils/data_science/dataset/__init__.py +45 -0
- stouputils/data_science/dataset/dataset.py +292 -0
- stouputils/data_science/dataset/dataset_loader.py +135 -0
- stouputils/data_science/dataset/grouping_strategy.py +296 -0
- stouputils/data_science/dataset/image_loader.py +100 -0
- stouputils/data_science/dataset/xy_tuple.py +696 -0
- stouputils/data_science/metric_dictionnary.py +106 -0
- stouputils/data_science/metric_utils.py +847 -0
- stouputils/data_science/mlflow_utils.py +206 -0
- stouputils/data_science/models/abstract_model.py +149 -0
- stouputils/data_science/models/all.py +85 -0
- stouputils/data_science/models/base_keras.py +765 -0
- stouputils/data_science/models/keras/all.py +38 -0
- stouputils/data_science/models/keras/convnext.py +62 -0
- stouputils/data_science/models/keras/densenet.py +50 -0
- stouputils/data_science/models/keras/efficientnet.py +60 -0
- stouputils/data_science/models/keras/mobilenet.py +56 -0
- stouputils/data_science/models/keras/resnet.py +52 -0
- stouputils/data_science/models/keras/squeezenet.py +233 -0
- stouputils/data_science/models/keras/vgg.py +42 -0
- stouputils/data_science/models/keras/xception.py +38 -0
- stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
- stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
- stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
- stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
- stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
- stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
- stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
- stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
- stouputils/data_science/models/keras_utils/visualizations.py +416 -0
- stouputils/data_science/models/model_interface.py +939 -0
- stouputils/data_science/models/sandbox.py +116 -0
- stouputils/data_science/range_tuple.py +234 -0
- stouputils/data_science/scripts/augment_dataset.py +77 -0
- stouputils/data_science/scripts/exhaustive_process.py +133 -0
- stouputils/data_science/scripts/preprocess_dataset.py +70 -0
- stouputils/data_science/scripts/routine.py +168 -0
- stouputils/data_science/utils.py +285 -0
- stouputils/decorators.py +605 -0
- stouputils/image.py +441 -0
- stouputils/installer/__init__.py +18 -0
- stouputils/installer/common.py +67 -0
- stouputils/installer/downloader.py +101 -0
- stouputils/installer/linux.py +144 -0
- stouputils/installer/main.py +223 -0
- stouputils/installer/windows.py +136 -0
- stouputils/io.py +486 -0
- stouputils/parallel.py +483 -0
- stouputils/print.py +482 -0
- stouputils/py.typed +1 -0
- stouputils/stouputils/__init__.pyi +15 -0
- stouputils/stouputils/_deprecated.pyi +12 -0
- stouputils/stouputils/all_doctests.pyi +46 -0
- stouputils/stouputils/applications/__init__.pyi +2 -0
- stouputils/stouputils/applications/automatic_docs.pyi +106 -0
- stouputils/stouputils/applications/upscaler/__init__.pyi +3 -0
- stouputils/stouputils/applications/upscaler/config.pyi +18 -0
- stouputils/stouputils/applications/upscaler/image.pyi +109 -0
- stouputils/stouputils/applications/upscaler/video.pyi +60 -0
- stouputils/stouputils/archive.pyi +67 -0
- stouputils/stouputils/backup.pyi +109 -0
- stouputils/stouputils/collections.pyi +86 -0
- stouputils/stouputils/continuous_delivery/__init__.pyi +5 -0
- stouputils/stouputils/continuous_delivery/cd_utils.pyi +129 -0
- stouputils/stouputils/continuous_delivery/github.pyi +162 -0
- stouputils/stouputils/continuous_delivery/pypi.pyi +53 -0
- stouputils/stouputils/continuous_delivery/pyproject.pyi +67 -0
- stouputils/stouputils/continuous_delivery/stubs.pyi +39 -0
- stouputils/stouputils/ctx.pyi +211 -0
- stouputils/stouputils/decorators.pyi +252 -0
- stouputils/stouputils/image.pyi +172 -0
- stouputils/stouputils/installer/__init__.pyi +5 -0
- stouputils/stouputils/installer/common.pyi +39 -0
- stouputils/stouputils/installer/downloader.pyi +24 -0
- stouputils/stouputils/installer/linux.pyi +39 -0
- stouputils/stouputils/installer/main.pyi +57 -0
- stouputils/stouputils/installer/windows.pyi +31 -0
- stouputils/stouputils/io.pyi +213 -0
- stouputils/stouputils/parallel.pyi +216 -0
- stouputils/stouputils/print.pyi +136 -0
- stouputils/stouputils/version_pkg.pyi +15 -0
- stouputils/version_pkg.py +189 -0
- stouputils-1.14.0.dist-info/METADATA +178 -0
- stouputils-1.14.0.dist-info/RECORD +140 -0
- stouputils-1.14.0.dist-info/WHEEL +4 -0
- stouputils-1.14.0.dist-info/entry_points.txt +3 -0
stouputils/decorators.py
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides decorators for various purposes:
|
|
3
|
+
|
|
4
|
+
- measure_time(): Measure the execution time of a function and print it with the given print function
|
|
5
|
+
- handle_error(): Handle an error with different log levels
|
|
6
|
+
- timeout(): Raise an exception if the function runs longer than the specified timeout
|
|
7
|
+
- retry(): Retry a function when specific exceptions are raised, with configurable delay and max attempts
|
|
8
|
+
- simple_cache(): Easy cache function with parameter caching method
|
|
9
|
+
- abstract(): Mark a function as abstract, using LogLevels for error handling
|
|
10
|
+
- deprecated(): Mark a function as deprecated, using LogLevels for warning handling
|
|
11
|
+
- silent(): Make a function silent (disable stdout, and stderr if specified) (alternative to stouputils.ctx.Muffle)
|
|
12
|
+
|
|
13
|
+
.. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/decorators_module_1.gif
|
|
14
|
+
:alt: stouputils decorators examples
|
|
15
|
+
|
|
16
|
+
.. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/decorators_module_2.gif
|
|
17
|
+
:alt: stouputils decorators examples
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Imports
|
|
21
|
+
import time
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from functools import wraps
|
|
25
|
+
from pickle import dumps as pickle_dumps
|
|
26
|
+
from traceback import format_exc
|
|
27
|
+
from typing import Any, Literal
|
|
28
|
+
|
|
29
|
+
from .ctx import MeasureTime, Muffle
|
|
30
|
+
from .print import error, progress, warning
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Execution time decorator
|
|
34
|
+
def measure_time(
|
|
35
|
+
func: Callable[..., Any] | None = None,
|
|
36
|
+
*,
|
|
37
|
+
printer: Callable[..., None] = progress,
|
|
38
|
+
message: str = "",
|
|
39
|
+
perf_counter: bool = True,
|
|
40
|
+
is_generator: bool = False
|
|
41
|
+
) -> Callable[..., Any]:
|
|
42
|
+
""" Decorator that will measure the execution time of a function and print it with the given print function
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
func (Callable[..., Any] | None): Function to decorate
|
|
46
|
+
printer (Callable): Function to use to print the execution time (e.g. debug, info, warning, error, etc.)
|
|
47
|
+
message (str): Message to display with the execution time (e.g. "Execution time of Something"),
|
|
48
|
+
defaults to "Execution time of {func.__name__}"
|
|
49
|
+
perf_counter (bool): Whether to use time.perf_counter_ns or time.time_ns
|
|
50
|
+
defaults to True (use time.perf_counter_ns)
|
|
51
|
+
is_generator (bool): Whether the function is a generator or not (default: False)
|
|
52
|
+
When True, the decorator will yield from the function instead of returning it.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Callable: Decorator to measure the time of the function.
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
.. code-block:: python
|
|
59
|
+
|
|
60
|
+
> @measure_time(printer=info)
|
|
61
|
+
> def test():
|
|
62
|
+
> pass
|
|
63
|
+
> test() # [INFO HH:MM:SS] Execution time of test: 0.000ms (400ns)
|
|
64
|
+
"""
|
|
65
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
66
|
+
# Set the message if not specified, else use the provided one
|
|
67
|
+
new_msg: str = message if message else f"Execution time of {_get_func_name(func)}()"
|
|
68
|
+
|
|
69
|
+
if is_generator:
|
|
70
|
+
@wraps(func)
|
|
71
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
|
|
72
|
+
with MeasureTime(print_func=printer, message=new_msg, perf_counter=perf_counter):
|
|
73
|
+
yield from func(*args, **kwargs)
|
|
74
|
+
else:
|
|
75
|
+
@wraps(func)
|
|
76
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
|
|
77
|
+
with MeasureTime(print_func=printer, message=new_msg, perf_counter=perf_counter):
|
|
78
|
+
return func(*args, **kwargs)
|
|
79
|
+
wrapper.__name__ = _get_wrapper_name("stouputils.decorators.measure_time", func)
|
|
80
|
+
return wrapper
|
|
81
|
+
|
|
82
|
+
# Handle both @measure_time and @measure_time(printer=..., message=..., perf_counter=..., is_generator=...)
|
|
83
|
+
if func is None:
|
|
84
|
+
return decorator
|
|
85
|
+
return decorator(func)
|
|
86
|
+
|
|
87
|
+
# Decorator that handle an error with different log levels
|
|
88
|
+
class LogLevels(Enum):
|
|
89
|
+
""" Log level for the errors in the decorator handle_error() """
|
|
90
|
+
NONE = 0
|
|
91
|
+
""" Do nothing """
|
|
92
|
+
WARNING = 1
|
|
93
|
+
""" Show as warning """
|
|
94
|
+
WARNING_TRACEBACK = 2
|
|
95
|
+
""" Show as warning with traceback """
|
|
96
|
+
ERROR_TRACEBACK = 3
|
|
97
|
+
""" Show as error with traceback """
|
|
98
|
+
RAISE_EXCEPTION = 4
|
|
99
|
+
""" Raise exception """
|
|
100
|
+
|
|
101
|
+
force_raise_exception: bool = False
|
|
102
|
+
""" If true, error_log parameter will be set to RAISE_EXCEPTION for every next handle_error calls, useful for doctests """
|
|
103
|
+
|
|
104
|
+
def handle_error(
|
|
105
|
+
func: Callable[..., Any] | None = None,
|
|
106
|
+
*,
|
|
107
|
+
exceptions: tuple[type[BaseException], ...] | type[BaseException] = (Exception,),
|
|
108
|
+
message: str = "",
|
|
109
|
+
error_log: LogLevels = LogLevels.WARNING_TRACEBACK,
|
|
110
|
+
sleep_time: float = 0.0
|
|
111
|
+
) -> Callable[..., Any]:
|
|
112
|
+
""" Decorator that handle an error with different log levels.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
func (Callable[..., Any] | None): Function to decorate
|
|
116
|
+
exceptions (tuple[type[BaseException]], ...): Exceptions to handle
|
|
117
|
+
message (str): Message to display with the error. (e.g. "Error during something")
|
|
118
|
+
error_log (LogLevels): Log level for the errors
|
|
119
|
+
LogLevels.NONE: None
|
|
120
|
+
LogLevels.WARNING: Show as warning
|
|
121
|
+
LogLevels.WARNING_TRACEBACK: Show as warning with traceback
|
|
122
|
+
LogLevels.ERROR_TRACEBACK: Show as error with traceback
|
|
123
|
+
LogLevels.RAISE_EXCEPTION: Raise exception
|
|
124
|
+
sleep_time (float): Time to sleep after the error (e.g. 0.0 to not sleep, 1.0 to sleep for 1 second)
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
>>> @handle_error
|
|
128
|
+
... def might_fail():
|
|
129
|
+
... raise ValueError("Let's fail")
|
|
130
|
+
|
|
131
|
+
>>> @handle_error(error_log=LogLevels.WARNING)
|
|
132
|
+
... def test():
|
|
133
|
+
... raise ValueError("Let's fail")
|
|
134
|
+
>>> # test() # [WARNING HH:MM:SS] Error during test: (ValueError) Let's fail
|
|
135
|
+
"""
|
|
136
|
+
# Update error_log if needed
|
|
137
|
+
if force_raise_exception:
|
|
138
|
+
error_log = LogLevels.RAISE_EXCEPTION
|
|
139
|
+
|
|
140
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
141
|
+
if message != "":
|
|
142
|
+
msg: str = f"{message}, "
|
|
143
|
+
else:
|
|
144
|
+
msg: str = message
|
|
145
|
+
|
|
146
|
+
@wraps(func)
|
|
147
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
|
|
148
|
+
try:
|
|
149
|
+
return func(*args, **kwargs)
|
|
150
|
+
except exceptions as e:
|
|
151
|
+
if error_log == LogLevels.WARNING:
|
|
152
|
+
warning(f"{msg}Error during {_get_func_name(func)}(): ({type(e).__name__}) {e}")
|
|
153
|
+
elif error_log == LogLevels.WARNING_TRACEBACK:
|
|
154
|
+
warning(f"{msg}Error during {_get_func_name(func)}():\n{format_exc()}")
|
|
155
|
+
elif error_log == LogLevels.ERROR_TRACEBACK:
|
|
156
|
+
error(f"{msg}Error during {_get_func_name(func)}():\n{format_exc()}", exit=True)
|
|
157
|
+
elif error_log == LogLevels.RAISE_EXCEPTION:
|
|
158
|
+
raise e
|
|
159
|
+
|
|
160
|
+
# Sleep for the specified time, only if the error_log is not ERROR_TRACEBACK (because it's blocking)
|
|
161
|
+
if sleep_time > 0.0 and error_log != LogLevels.ERROR_TRACEBACK:
|
|
162
|
+
time.sleep(sleep_time)
|
|
163
|
+
wrapper.__name__ = _get_wrapper_name("stouputils.decorators.handle_error", func)
|
|
164
|
+
return wrapper
|
|
165
|
+
|
|
166
|
+
# Handle both @handle_error and @handle_error(exceptions=..., message=..., error_log=...)
|
|
167
|
+
if func is None:
|
|
168
|
+
return decorator
|
|
169
|
+
return decorator(func)
|
|
170
|
+
|
|
171
|
+
# Decorator that raises an exception if the function runs too long
|
|
172
|
+
def timeout(
|
|
173
|
+
func: Callable[..., Any] | None = None,
|
|
174
|
+
*,
|
|
175
|
+
seconds: float = 60.0,
|
|
176
|
+
message: str = ""
|
|
177
|
+
) -> Callable[..., Any]:
|
|
178
|
+
""" Decorator that raises a TimeoutError if the function runs longer than the specified timeout.
|
|
179
|
+
|
|
180
|
+
Note: This decorator uses SIGALRM on Unix systems, which only works in the main thread.
|
|
181
|
+
On Windows or in non-main threads, it will fall back to a polling-based approach.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
func (Callable[..., Any] | None): Function to apply timeout to
|
|
185
|
+
seconds (float): Timeout duration in seconds (default: 60.0)
|
|
186
|
+
message (str): Custom timeout message (default: "Function '{func_name}' timed out after {seconds} seconds")
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Callable[..., Any]: Decorator that enforces timeout on the function
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
TimeoutError: If the function execution exceeds the timeout duration
|
|
193
|
+
|
|
194
|
+
Examples:
|
|
195
|
+
>>> @timeout(seconds=2.0)
|
|
196
|
+
... def slow_function():
|
|
197
|
+
... time.sleep(5)
|
|
198
|
+
>>> slow_function() # Raises TimeoutError after 2 seconds
|
|
199
|
+
Traceback (most recent call last):
|
|
200
|
+
...
|
|
201
|
+
TimeoutError: Function 'slow_function' timed out after 2.0 seconds
|
|
202
|
+
|
|
203
|
+
>>> @timeout(seconds=1.0, message="Custom timeout message")
|
|
204
|
+
... def another_slow_function():
|
|
205
|
+
... time.sleep(3)
|
|
206
|
+
>>> another_slow_function() # Raises TimeoutError after 1 second
|
|
207
|
+
Traceback (most recent call last):
|
|
208
|
+
...
|
|
209
|
+
TimeoutError: Custom timeout message
|
|
210
|
+
"""
|
|
211
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
212
|
+
@wraps(func)
|
|
213
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
|
|
214
|
+
# Build timeout message
|
|
215
|
+
msg: str = message if message else f"Function '{_get_func_name(func)}()' timed out after {seconds} seconds"
|
|
216
|
+
|
|
217
|
+
# Try to use signal-based timeout (Unix only, main thread only)
|
|
218
|
+
try:
|
|
219
|
+
import signal
|
|
220
|
+
def timeout_handler(signum: int, frame: Any) -> None:
|
|
221
|
+
raise TimeoutError(msg)
|
|
222
|
+
|
|
223
|
+
# Set the signal handler and alarm
|
|
224
|
+
old_handler = signal.signal(signal.SIGALRM, timeout_handler) # type: ignore
|
|
225
|
+
signal.setitimer(signal.ITIMER_REAL, seconds) # type: ignore
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
result = func(*args, **kwargs)
|
|
229
|
+
finally:
|
|
230
|
+
# Cancel the alarm and restore the old handler
|
|
231
|
+
signal.setitimer(signal.ITIMER_REAL, 0) # type: ignore
|
|
232
|
+
signal.signal(signal.SIGALRM, old_handler) # type: ignore
|
|
233
|
+
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
except (ValueError, AttributeError) as e:
|
|
237
|
+
# SIGALRM not available (Windows) or not in main thread
|
|
238
|
+
# Fall back to polling-based timeout (less precise but portable)
|
|
239
|
+
import threading
|
|
240
|
+
|
|
241
|
+
result_container: list[Any] = []
|
|
242
|
+
exception_container: list[BaseException] = []
|
|
243
|
+
|
|
244
|
+
def target() -> None:
|
|
245
|
+
try:
|
|
246
|
+
result_container.append(func(*args, **kwargs))
|
|
247
|
+
except BaseException as e_2:
|
|
248
|
+
exception_container.append(e_2)
|
|
249
|
+
|
|
250
|
+
thread = threading.Thread(target=target, daemon=True)
|
|
251
|
+
thread.start()
|
|
252
|
+
thread.join(timeout=seconds)
|
|
253
|
+
|
|
254
|
+
if thread.is_alive():
|
|
255
|
+
# Thread is still running, timeout occurred
|
|
256
|
+
raise TimeoutError(msg) from e
|
|
257
|
+
|
|
258
|
+
# Check if an exception was raised in the thread
|
|
259
|
+
if exception_container:
|
|
260
|
+
raise exception_container[0] from e
|
|
261
|
+
|
|
262
|
+
# Return the result if available
|
|
263
|
+
if result_container:
|
|
264
|
+
return result_container[0]
|
|
265
|
+
|
|
266
|
+
wrapper.__name__ = _get_wrapper_name("stouputils.decorators.timeout", func)
|
|
267
|
+
return wrapper
|
|
268
|
+
|
|
269
|
+
# Handle both @timeout and @timeout(seconds=..., message=...)
|
|
270
|
+
if func is None:
|
|
271
|
+
return decorator
|
|
272
|
+
return decorator(func)
|
|
273
|
+
|
|
274
|
+
# Decorator that retries a function when specific exceptions are raised
|
|
275
|
+
def retry(
|
|
276
|
+
func: Callable[..., Any] | None = None,
|
|
277
|
+
*,
|
|
278
|
+
exceptions: tuple[type[BaseException], ...] | type[BaseException] = (Exception,),
|
|
279
|
+
max_attempts: int = 10,
|
|
280
|
+
delay: float = 1.0,
|
|
281
|
+
backoff: float = 1.0,
|
|
282
|
+
message: str = ""
|
|
283
|
+
) -> Callable[..., Any]:
|
|
284
|
+
""" Decorator that retries a function when specific exceptions are raised.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
func (Callable[..., Any] | None): Function to retry
|
|
288
|
+
exceptions (tuple[type[BaseException], ...]): Exceptions to catch and retry on
|
|
289
|
+
max_attempts (int | None): Maximum number of attempts (None for infinite retries)
|
|
290
|
+
delay (float): Initial delay in seconds between retries (default: 1.0)
|
|
291
|
+
backoff (float): Multiplier for delay after each retry (default: 1.0 for constant delay)
|
|
292
|
+
message (str): Custom message to display before ", retrying" (default: "{ExceptionName} encountered while running {func_name}")
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Callable[..., Any]: Decorator that retries the function on specified exceptions
|
|
296
|
+
|
|
297
|
+
Examples:
|
|
298
|
+
>>> import os
|
|
299
|
+
>>> @retry(exceptions=PermissionError, max_attempts=3, delay=0.1)
|
|
300
|
+
... def write_file():
|
|
301
|
+
... with open("test.txt", "w") as f:
|
|
302
|
+
... f.write("test")
|
|
303
|
+
|
|
304
|
+
>>> @retry(exceptions=(OSError, IOError), delay=0.5, backoff=2.0)
|
|
305
|
+
... def network_call():
|
|
306
|
+
... pass
|
|
307
|
+
|
|
308
|
+
>>> @retry(max_attempts=5, delay=1.0)
|
|
309
|
+
... def might_fail():
|
|
310
|
+
... pass
|
|
311
|
+
"""
|
|
312
|
+
# Normalize exceptions to tuple
|
|
313
|
+
if not isinstance(exceptions, tuple):
|
|
314
|
+
exceptions = (exceptions,)
|
|
315
|
+
|
|
316
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
317
|
+
@wraps(func)
|
|
318
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
|
|
319
|
+
attempt: int = 0
|
|
320
|
+
current_delay: float = delay
|
|
321
|
+
|
|
322
|
+
while True:
|
|
323
|
+
attempt += 1
|
|
324
|
+
try:
|
|
325
|
+
return func(*args, **kwargs)
|
|
326
|
+
except exceptions as e:
|
|
327
|
+
# Check if we should retry or give up
|
|
328
|
+
if max_attempts != 1 and attempt >= max_attempts:
|
|
329
|
+
raise e
|
|
330
|
+
|
|
331
|
+
# Log retry attempt
|
|
332
|
+
if message:
|
|
333
|
+
warning(f"{message}, retrying ({attempt + 1}/{max_attempts}): {e}")
|
|
334
|
+
else:
|
|
335
|
+
warning(f"{type(e).__name__} encountered while running {_get_func_name(func)}(), retrying ({attempt + 1}/{max_attempts}): {e}")
|
|
336
|
+
|
|
337
|
+
# Wait before next attempt
|
|
338
|
+
time.sleep(current_delay)
|
|
339
|
+
current_delay *= backoff
|
|
340
|
+
|
|
341
|
+
wrapper.__name__ = _get_wrapper_name("stouputils.decorators.retry", func)
|
|
342
|
+
return wrapper
|
|
343
|
+
|
|
344
|
+
# Handle both @retry and @retry(exceptions=..., max_attempts=..., delay=...)
|
|
345
|
+
if func is None:
|
|
346
|
+
return decorator
|
|
347
|
+
return decorator(func)
|
|
348
|
+
|
|
349
|
+
# Easy cache function with parameter caching method
|
|
350
|
+
def simple_cache(
|
|
351
|
+
func: Callable[..., Any] | None = None,
|
|
352
|
+
*,
|
|
353
|
+
method: Literal["str", "pickle"] = "str"
|
|
354
|
+
) -> Callable[..., Any]:
|
|
355
|
+
""" Decorator that caches the result of a function based on its arguments.
|
|
356
|
+
|
|
357
|
+
The str method is often faster than the pickle method (by a little) but not as accurate with complex objects.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
func (Callable[..., Any] | None): Function to cache
|
|
361
|
+
method (Literal["str", "pickle"]): The method to use for caching.
|
|
362
|
+
Returns:
|
|
363
|
+
Callable[..., Any]: A decorator that caches the result of a function.
|
|
364
|
+
Examples:
|
|
365
|
+
>>> @simple_cache
|
|
366
|
+
... def test1(a: int, b: int) -> int:
|
|
367
|
+
... return a + b
|
|
368
|
+
|
|
369
|
+
>>> @simple_cache(method="str")
|
|
370
|
+
... def test2(a: int, b: int) -> int:
|
|
371
|
+
... return a + b
|
|
372
|
+
>>> test2(1, 2)
|
|
373
|
+
3
|
|
374
|
+
>>> test2(1, 2)
|
|
375
|
+
3
|
|
376
|
+
>>> test2(3, 4)
|
|
377
|
+
7
|
|
378
|
+
|
|
379
|
+
>>> @simple_cache
|
|
380
|
+
... def factorial(n: int) -> int:
|
|
381
|
+
... return n * factorial(n - 1) if n else 1
|
|
382
|
+
>>> factorial(10) # no previously cached result, makes 11 recursive calls
|
|
383
|
+
3628800
|
|
384
|
+
>>> factorial(5) # no new calls, just returns the cached result
|
|
385
|
+
120
|
|
386
|
+
>>> factorial(12) # two new recursive calls, factorial(10) is cached
|
|
387
|
+
479001600
|
|
388
|
+
"""
|
|
389
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
390
|
+
# Create the cache dict
|
|
391
|
+
cache_dict: dict[Any, Any] = {}
|
|
392
|
+
|
|
393
|
+
# Create the wrapper
|
|
394
|
+
@wraps(func)
|
|
395
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
|
|
396
|
+
|
|
397
|
+
# Get the hashed key
|
|
398
|
+
if method == "str":
|
|
399
|
+
hashed = str(args) + str(kwargs)
|
|
400
|
+
elif method == "pickle":
|
|
401
|
+
hashed = pickle_dumps((args, kwargs))
|
|
402
|
+
else:
|
|
403
|
+
raise ValueError("Invalid caching method. Supported methods are 'str' and 'pickle'.")
|
|
404
|
+
|
|
405
|
+
# If the key is in the cache, return it
|
|
406
|
+
if hashed in cache_dict:
|
|
407
|
+
return cache_dict[hashed]
|
|
408
|
+
|
|
409
|
+
# Else, call the function and add the result to the cache
|
|
410
|
+
else:
|
|
411
|
+
result: Any = func(*args, **kwargs)
|
|
412
|
+
cache_dict[hashed] = result
|
|
413
|
+
return result
|
|
414
|
+
|
|
415
|
+
# Return the wrapper
|
|
416
|
+
wrapper.__name__ = _get_wrapper_name("stouputils.decorators.simple_cache", func)
|
|
417
|
+
return wrapper
|
|
418
|
+
|
|
419
|
+
# Handle both @simple_cache and @simple_cache(method=...)
|
|
420
|
+
if func is None:
|
|
421
|
+
return decorator
|
|
422
|
+
return decorator(func)
|
|
423
|
+
|
|
424
|
+
# Decorator that marks a function as abstract
|
|
425
|
+
def abstract(
|
|
426
|
+
func: Callable[..., Any] | None = None,
|
|
427
|
+
*,
|
|
428
|
+
error_log: LogLevels = LogLevels.RAISE_EXCEPTION
|
|
429
|
+
) -> Callable[..., Any]:
|
|
430
|
+
""" Decorator that marks a function as abstract.
|
|
431
|
+
|
|
432
|
+
Contrary to the abstractmethod decorator from the abc module that raises a TypeError
|
|
433
|
+
when you try to instantiate a class that has abstract methods, this decorator raises
|
|
434
|
+
a NotImplementedError ONLY when the decorated function is called, indicating that the function
|
|
435
|
+
must be implemented by a subclass.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
func (Callable[..., Any] | None): The function to mark as abstract
|
|
439
|
+
error_log (LogLevels): Log level for the error handling
|
|
440
|
+
LogLevels.NONE: None
|
|
441
|
+
LogLevels.WARNING: Show as warning
|
|
442
|
+
LogLevels.WARNING_TRACEBACK: Show as warning with traceback
|
|
443
|
+
LogLevels.ERROR_TRACEBACK: Show as error with traceback
|
|
444
|
+
LogLevels.RAISE_EXCEPTION: Raise exception
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Callable[..., Any]: Decorator that raises NotImplementedError when called
|
|
448
|
+
|
|
449
|
+
Examples:
|
|
450
|
+
>>> class Base:
|
|
451
|
+
... @abstract
|
|
452
|
+
... def method(self):
|
|
453
|
+
... pass
|
|
454
|
+
>>> Base().method()
|
|
455
|
+
Traceback (most recent call last):
|
|
456
|
+
...
|
|
457
|
+
NotImplementedError: Function 'method' is abstract and must be implemented by a subclass
|
|
458
|
+
"""
|
|
459
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
460
|
+
message: str = f"Function '{_get_func_name(func)}()' is abstract and must be implemented by a subclass"
|
|
461
|
+
if not func.__doc__:
|
|
462
|
+
func.__doc__ = message
|
|
463
|
+
|
|
464
|
+
@wraps(func)
|
|
465
|
+
@handle_error(exceptions=NotImplementedError, error_log=error_log)
|
|
466
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
|
|
467
|
+
raise NotImplementedError(message)
|
|
468
|
+
wrapper.__name__ = _get_wrapper_name("stouputils.decorators.abstract", func)
|
|
469
|
+
return wrapper
|
|
470
|
+
|
|
471
|
+
# Handle both @abstract and @abstract(error_log=...)
|
|
472
|
+
if func is None:
|
|
473
|
+
return decorator
|
|
474
|
+
return decorator(func)
|
|
475
|
+
|
|
476
|
+
# Decorator that marks a function as deprecated
|
|
477
|
+
def deprecated(
|
|
478
|
+
func: Callable[..., Any] | None = None,
|
|
479
|
+
*,
|
|
480
|
+
message: str = "",
|
|
481
|
+
version: str = "",
|
|
482
|
+
error_log: LogLevels = LogLevels.WARNING
|
|
483
|
+
) -> Callable[..., Any]:
|
|
484
|
+
""" Decorator that marks a function as deprecated.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
func (Callable[..., Any] | None): Function to mark as deprecated
|
|
488
|
+
message (str): Additional message to display with the deprecation warning
|
|
489
|
+
version (str): Version since when the function is deprecated (e.g. "v1.2.0")
|
|
490
|
+
error_log (LogLevels): Log level for the deprecation warning
|
|
491
|
+
LogLevels.NONE: None
|
|
492
|
+
LogLevels.WARNING: Show as warning
|
|
493
|
+
LogLevels.WARNING_TRACEBACK: Show as warning with traceback
|
|
494
|
+
LogLevels.ERROR_TRACEBACK: Show as error with traceback
|
|
495
|
+
LogLevels.RAISE_EXCEPTION: Raise exception
|
|
496
|
+
Returns:
|
|
497
|
+
Callable[..., Any]: Decorator that marks a function as deprecated
|
|
498
|
+
|
|
499
|
+
Examples:
|
|
500
|
+
>>> @deprecated
|
|
501
|
+
... def old_function():
|
|
502
|
+
... pass
|
|
503
|
+
|
|
504
|
+
>>> @deprecated(message="Use 'new_function()' instead", error_log=LogLevels.WARNING)
|
|
505
|
+
... def another_old_function():
|
|
506
|
+
... pass
|
|
507
|
+
"""
|
|
508
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
509
|
+
@wraps(func)
|
|
510
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
|
|
511
|
+
# Build deprecation message
|
|
512
|
+
msg: str = f"Function '{_get_func_name(func)}()' is deprecated"
|
|
513
|
+
if version:
|
|
514
|
+
msg += f" since {version}"
|
|
515
|
+
if message:
|
|
516
|
+
msg += f". {message}"
|
|
517
|
+
|
|
518
|
+
# Handle deprecation warning based on log level
|
|
519
|
+
if error_log == LogLevels.WARNING:
|
|
520
|
+
warning(msg)
|
|
521
|
+
elif error_log == LogLevels.WARNING_TRACEBACK:
|
|
522
|
+
warning(f"{msg}\n{format_exc()}")
|
|
523
|
+
elif error_log == LogLevels.ERROR_TRACEBACK:
|
|
524
|
+
error(f"{msg}\n{format_exc()}", exit=True)
|
|
525
|
+
elif error_log == LogLevels.RAISE_EXCEPTION:
|
|
526
|
+
raise DeprecationWarning(msg)
|
|
527
|
+
|
|
528
|
+
# Call the original function
|
|
529
|
+
return func(*args, **kwargs)
|
|
530
|
+
wrapper.__name__ = _get_wrapper_name("stouputils.decorators.deprecated", func)
|
|
531
|
+
return wrapper
|
|
532
|
+
|
|
533
|
+
# Handle both @deprecated and @deprecated(message=..., error_log=...)
|
|
534
|
+
if func is None:
|
|
535
|
+
return decorator
|
|
536
|
+
return decorator(func)
|
|
537
|
+
|
|
538
|
+
# Decorator that make a function silent (disable stdout)
|
|
539
|
+
def silent(
|
|
540
|
+
func: Callable[..., Any] | None = None,
|
|
541
|
+
*,
|
|
542
|
+
mute_stderr: bool = False
|
|
543
|
+
) -> Callable[..., Any]:
|
|
544
|
+
""" Decorator that makes a function silent (disable stdout, and stderr if specified).
|
|
545
|
+
|
|
546
|
+
Alternative to stouputils.ctx.Muffle.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
func (Callable[..., Any] | None): Function to make silent
|
|
550
|
+
mute_stderr (bool): Whether to mute stderr or not
|
|
551
|
+
|
|
552
|
+
Examples:
|
|
553
|
+
>>> @silent
|
|
554
|
+
... def test():
|
|
555
|
+
... print("Hello, world!")
|
|
556
|
+
>>> test()
|
|
557
|
+
|
|
558
|
+
>>> @silent(mute_stderr=True)
|
|
559
|
+
... def test2():
|
|
560
|
+
... print("Hello, world!")
|
|
561
|
+
>>> test2()
|
|
562
|
+
|
|
563
|
+
>>> silent(print)("Hello, world!")
|
|
564
|
+
"""
|
|
565
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
566
|
+
@wraps(func)
|
|
567
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
|
|
568
|
+
# Use Muffle context manager to silence output
|
|
569
|
+
with Muffle(mute_stderr=mute_stderr):
|
|
570
|
+
return func(*args, **kwargs)
|
|
571
|
+
wrapper.__name__ = _get_wrapper_name("stouputils.decorators.silent", func)
|
|
572
|
+
return wrapper
|
|
573
|
+
|
|
574
|
+
# Handle both @silent and @silent(mute_stderr=...)
|
|
575
|
+
if func is None:
|
|
576
|
+
return decorator
|
|
577
|
+
return decorator(func)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# "Private" functions
|
|
582
|
+
def _get_func_name(func: Callable[..., Any]) -> str:
|
|
583
|
+
""" Get the name of a function, returns "<unknown>" if the name cannot be retrieved. """
|
|
584
|
+
try:
|
|
585
|
+
return func.__name__
|
|
586
|
+
except AttributeError:
|
|
587
|
+
return "<unknown>"
|
|
588
|
+
|
|
589
|
+
def _get_wrapper_name(decorator_name: str, func: Callable[..., Any]) -> str:
|
|
590
|
+
""" Get a descriptive name for a wrapper function.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
decorator_name (str): Name of the decorator
|
|
594
|
+
func (Callable[..., Any]): Function being decorated
|
|
595
|
+
Returns:
|
|
596
|
+
str: Combined name for the wrapper function (e.g., "stouputils.decorators.handle_error@function_name")
|
|
597
|
+
"""
|
|
598
|
+
func_name: str = _get_func_name(func)
|
|
599
|
+
|
|
600
|
+
# Remove "stouputils.decorators.*" prefix if present
|
|
601
|
+
if func_name.startswith("stouputils.decorators."):
|
|
602
|
+
func_name = ".".join(func_name.split(".")[3:])
|
|
603
|
+
|
|
604
|
+
return f"{decorator_name}@{func_name}"
|
|
605
|
+
|