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.
Files changed (140) hide show
  1. stouputils/__init__.py +40 -0
  2. stouputils/__main__.py +86 -0
  3. stouputils/_deprecated.py +37 -0
  4. stouputils/all_doctests.py +160 -0
  5. stouputils/applications/__init__.py +22 -0
  6. stouputils/applications/automatic_docs.py +634 -0
  7. stouputils/applications/upscaler/__init__.py +39 -0
  8. stouputils/applications/upscaler/config.py +128 -0
  9. stouputils/applications/upscaler/image.py +247 -0
  10. stouputils/applications/upscaler/video.py +287 -0
  11. stouputils/archive.py +344 -0
  12. stouputils/backup.py +488 -0
  13. stouputils/collections.py +244 -0
  14. stouputils/continuous_delivery/__init__.py +27 -0
  15. stouputils/continuous_delivery/cd_utils.py +243 -0
  16. stouputils/continuous_delivery/github.py +522 -0
  17. stouputils/continuous_delivery/pypi.py +130 -0
  18. stouputils/continuous_delivery/pyproject.py +147 -0
  19. stouputils/continuous_delivery/stubs.py +86 -0
  20. stouputils/ctx.py +408 -0
  21. stouputils/data_science/config/get.py +51 -0
  22. stouputils/data_science/config/set.py +125 -0
  23. stouputils/data_science/data_processing/image/__init__.py +66 -0
  24. stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
  25. stouputils/data_science/data_processing/image/axis_flip.py +58 -0
  26. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
  27. stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
  28. stouputils/data_science/data_processing/image/blur.py +59 -0
  29. stouputils/data_science/data_processing/image/brightness.py +54 -0
  30. stouputils/data_science/data_processing/image/canny.py +110 -0
  31. stouputils/data_science/data_processing/image/clahe.py +92 -0
  32. stouputils/data_science/data_processing/image/common.py +30 -0
  33. stouputils/data_science/data_processing/image/contrast.py +53 -0
  34. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
  35. stouputils/data_science/data_processing/image/denoise.py +378 -0
  36. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
  37. stouputils/data_science/data_processing/image/invert.py +64 -0
  38. stouputils/data_science/data_processing/image/laplacian.py +60 -0
  39. stouputils/data_science/data_processing/image/median_blur.py +52 -0
  40. stouputils/data_science/data_processing/image/noise.py +59 -0
  41. stouputils/data_science/data_processing/image/normalize.py +65 -0
  42. stouputils/data_science/data_processing/image/random_erase.py +66 -0
  43. stouputils/data_science/data_processing/image/resize.py +69 -0
  44. stouputils/data_science/data_processing/image/rotation.py +80 -0
  45. stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
  46. stouputils/data_science/data_processing/image/sharpening.py +55 -0
  47. stouputils/data_science/data_processing/image/shearing.py +64 -0
  48. stouputils/data_science/data_processing/image/threshold.py +64 -0
  49. stouputils/data_science/data_processing/image/translation.py +71 -0
  50. stouputils/data_science/data_processing/image/zoom.py +83 -0
  51. stouputils/data_science/data_processing/image_augmentation.py +118 -0
  52. stouputils/data_science/data_processing/image_preprocess.py +183 -0
  53. stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
  54. stouputils/data_science/data_processing/technique.py +481 -0
  55. stouputils/data_science/dataset/__init__.py +45 -0
  56. stouputils/data_science/dataset/dataset.py +292 -0
  57. stouputils/data_science/dataset/dataset_loader.py +135 -0
  58. stouputils/data_science/dataset/grouping_strategy.py +296 -0
  59. stouputils/data_science/dataset/image_loader.py +100 -0
  60. stouputils/data_science/dataset/xy_tuple.py +696 -0
  61. stouputils/data_science/metric_dictionnary.py +106 -0
  62. stouputils/data_science/metric_utils.py +847 -0
  63. stouputils/data_science/mlflow_utils.py +206 -0
  64. stouputils/data_science/models/abstract_model.py +149 -0
  65. stouputils/data_science/models/all.py +85 -0
  66. stouputils/data_science/models/base_keras.py +765 -0
  67. stouputils/data_science/models/keras/all.py +38 -0
  68. stouputils/data_science/models/keras/convnext.py +62 -0
  69. stouputils/data_science/models/keras/densenet.py +50 -0
  70. stouputils/data_science/models/keras/efficientnet.py +60 -0
  71. stouputils/data_science/models/keras/mobilenet.py +56 -0
  72. stouputils/data_science/models/keras/resnet.py +52 -0
  73. stouputils/data_science/models/keras/squeezenet.py +233 -0
  74. stouputils/data_science/models/keras/vgg.py +42 -0
  75. stouputils/data_science/models/keras/xception.py +38 -0
  76. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
  77. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
  78. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
  79. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
  80. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
  81. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
  82. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
  83. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
  84. stouputils/data_science/models/keras_utils/visualizations.py +416 -0
  85. stouputils/data_science/models/model_interface.py +939 -0
  86. stouputils/data_science/models/sandbox.py +116 -0
  87. stouputils/data_science/range_tuple.py +234 -0
  88. stouputils/data_science/scripts/augment_dataset.py +77 -0
  89. stouputils/data_science/scripts/exhaustive_process.py +133 -0
  90. stouputils/data_science/scripts/preprocess_dataset.py +70 -0
  91. stouputils/data_science/scripts/routine.py +168 -0
  92. stouputils/data_science/utils.py +285 -0
  93. stouputils/decorators.py +605 -0
  94. stouputils/image.py +441 -0
  95. stouputils/installer/__init__.py +18 -0
  96. stouputils/installer/common.py +67 -0
  97. stouputils/installer/downloader.py +101 -0
  98. stouputils/installer/linux.py +144 -0
  99. stouputils/installer/main.py +223 -0
  100. stouputils/installer/windows.py +136 -0
  101. stouputils/io.py +486 -0
  102. stouputils/parallel.py +483 -0
  103. stouputils/print.py +482 -0
  104. stouputils/py.typed +1 -0
  105. stouputils/stouputils/__init__.pyi +15 -0
  106. stouputils/stouputils/_deprecated.pyi +12 -0
  107. stouputils/stouputils/all_doctests.pyi +46 -0
  108. stouputils/stouputils/applications/__init__.pyi +2 -0
  109. stouputils/stouputils/applications/automatic_docs.pyi +106 -0
  110. stouputils/stouputils/applications/upscaler/__init__.pyi +3 -0
  111. stouputils/stouputils/applications/upscaler/config.pyi +18 -0
  112. stouputils/stouputils/applications/upscaler/image.pyi +109 -0
  113. stouputils/stouputils/applications/upscaler/video.pyi +60 -0
  114. stouputils/stouputils/archive.pyi +67 -0
  115. stouputils/stouputils/backup.pyi +109 -0
  116. stouputils/stouputils/collections.pyi +86 -0
  117. stouputils/stouputils/continuous_delivery/__init__.pyi +5 -0
  118. stouputils/stouputils/continuous_delivery/cd_utils.pyi +129 -0
  119. stouputils/stouputils/continuous_delivery/github.pyi +162 -0
  120. stouputils/stouputils/continuous_delivery/pypi.pyi +53 -0
  121. stouputils/stouputils/continuous_delivery/pyproject.pyi +67 -0
  122. stouputils/stouputils/continuous_delivery/stubs.pyi +39 -0
  123. stouputils/stouputils/ctx.pyi +211 -0
  124. stouputils/stouputils/decorators.pyi +252 -0
  125. stouputils/stouputils/image.pyi +172 -0
  126. stouputils/stouputils/installer/__init__.pyi +5 -0
  127. stouputils/stouputils/installer/common.pyi +39 -0
  128. stouputils/stouputils/installer/downloader.pyi +24 -0
  129. stouputils/stouputils/installer/linux.pyi +39 -0
  130. stouputils/stouputils/installer/main.pyi +57 -0
  131. stouputils/stouputils/installer/windows.pyi +31 -0
  132. stouputils/stouputils/io.pyi +213 -0
  133. stouputils/stouputils/parallel.pyi +216 -0
  134. stouputils/stouputils/print.pyi +136 -0
  135. stouputils/stouputils/version_pkg.pyi +15 -0
  136. stouputils/version_pkg.py +189 -0
  137. stouputils-1.14.0.dist-info/METADATA +178 -0
  138. stouputils-1.14.0.dist-info/RECORD +140 -0
  139. stouputils-1.14.0.dist-info/WHEEL +4 -0
  140. stouputils-1.14.0.dist-info/entry_points.txt +3 -0
@@ -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
+