stouputils 1.12.1__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 (138) hide show
  1. stouputils/__init__.py +40 -0
  2. stouputils/__init__.pyi +14 -0
  3. stouputils/__main__.py +81 -0
  4. stouputils/_deprecated.py +37 -0
  5. stouputils/_deprecated.pyi +12 -0
  6. stouputils/all_doctests.py +160 -0
  7. stouputils/all_doctests.pyi +46 -0
  8. stouputils/applications/__init__.py +22 -0
  9. stouputils/applications/__init__.pyi +2 -0
  10. stouputils/applications/automatic_docs.py +634 -0
  11. stouputils/applications/automatic_docs.pyi +106 -0
  12. stouputils/applications/upscaler/__init__.py +39 -0
  13. stouputils/applications/upscaler/__init__.pyi +3 -0
  14. stouputils/applications/upscaler/config.py +128 -0
  15. stouputils/applications/upscaler/config.pyi +18 -0
  16. stouputils/applications/upscaler/image.py +247 -0
  17. stouputils/applications/upscaler/image.pyi +109 -0
  18. stouputils/applications/upscaler/video.py +287 -0
  19. stouputils/applications/upscaler/video.pyi +60 -0
  20. stouputils/archive.py +344 -0
  21. stouputils/archive.pyi +67 -0
  22. stouputils/backup.py +488 -0
  23. stouputils/backup.pyi +109 -0
  24. stouputils/collections.py +244 -0
  25. stouputils/collections.pyi +86 -0
  26. stouputils/continuous_delivery/__init__.py +27 -0
  27. stouputils/continuous_delivery/__init__.pyi +5 -0
  28. stouputils/continuous_delivery/cd_utils.py +243 -0
  29. stouputils/continuous_delivery/cd_utils.pyi +129 -0
  30. stouputils/continuous_delivery/github.py +522 -0
  31. stouputils/continuous_delivery/github.pyi +162 -0
  32. stouputils/continuous_delivery/pypi.py +91 -0
  33. stouputils/continuous_delivery/pypi.pyi +43 -0
  34. stouputils/continuous_delivery/pyproject.py +147 -0
  35. stouputils/continuous_delivery/pyproject.pyi +67 -0
  36. stouputils/continuous_delivery/stubs.py +86 -0
  37. stouputils/continuous_delivery/stubs.pyi +39 -0
  38. stouputils/ctx.py +408 -0
  39. stouputils/ctx.pyi +211 -0
  40. stouputils/data_science/config/get.py +51 -0
  41. stouputils/data_science/config/set.py +125 -0
  42. stouputils/data_science/data_processing/image/__init__.py +66 -0
  43. stouputils/data_science/data_processing/image/auto_contrast.py +79 -0
  44. stouputils/data_science/data_processing/image/axis_flip.py +58 -0
  45. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -0
  46. stouputils/data_science/data_processing/image/binary_threshold.py +73 -0
  47. stouputils/data_science/data_processing/image/blur.py +59 -0
  48. stouputils/data_science/data_processing/image/brightness.py +54 -0
  49. stouputils/data_science/data_processing/image/canny.py +110 -0
  50. stouputils/data_science/data_processing/image/clahe.py +92 -0
  51. stouputils/data_science/data_processing/image/common.py +30 -0
  52. stouputils/data_science/data_processing/image/contrast.py +53 -0
  53. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -0
  54. stouputils/data_science/data_processing/image/denoise.py +378 -0
  55. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -0
  56. stouputils/data_science/data_processing/image/invert.py +64 -0
  57. stouputils/data_science/data_processing/image/laplacian.py +60 -0
  58. stouputils/data_science/data_processing/image/median_blur.py +52 -0
  59. stouputils/data_science/data_processing/image/noise.py +59 -0
  60. stouputils/data_science/data_processing/image/normalize.py +65 -0
  61. stouputils/data_science/data_processing/image/random_erase.py +66 -0
  62. stouputils/data_science/data_processing/image/resize.py +69 -0
  63. stouputils/data_science/data_processing/image/rotation.py +80 -0
  64. stouputils/data_science/data_processing/image/salt_pepper.py +68 -0
  65. stouputils/data_science/data_processing/image/sharpening.py +55 -0
  66. stouputils/data_science/data_processing/image/shearing.py +64 -0
  67. stouputils/data_science/data_processing/image/threshold.py +64 -0
  68. stouputils/data_science/data_processing/image/translation.py +71 -0
  69. stouputils/data_science/data_processing/image/zoom.py +83 -0
  70. stouputils/data_science/data_processing/image_augmentation.py +118 -0
  71. stouputils/data_science/data_processing/image_preprocess.py +183 -0
  72. stouputils/data_science/data_processing/prosthesis_detection.py +359 -0
  73. stouputils/data_science/data_processing/technique.py +481 -0
  74. stouputils/data_science/dataset/__init__.py +45 -0
  75. stouputils/data_science/dataset/dataset.py +292 -0
  76. stouputils/data_science/dataset/dataset_loader.py +135 -0
  77. stouputils/data_science/dataset/grouping_strategy.py +296 -0
  78. stouputils/data_science/dataset/image_loader.py +100 -0
  79. stouputils/data_science/dataset/xy_tuple.py +696 -0
  80. stouputils/data_science/metric_dictionnary.py +106 -0
  81. stouputils/data_science/metric_utils.py +847 -0
  82. stouputils/data_science/mlflow_utils.py +206 -0
  83. stouputils/data_science/models/abstract_model.py +149 -0
  84. stouputils/data_science/models/all.py +85 -0
  85. stouputils/data_science/models/base_keras.py +765 -0
  86. stouputils/data_science/models/keras/all.py +38 -0
  87. stouputils/data_science/models/keras/convnext.py +62 -0
  88. stouputils/data_science/models/keras/densenet.py +50 -0
  89. stouputils/data_science/models/keras/efficientnet.py +60 -0
  90. stouputils/data_science/models/keras/mobilenet.py +56 -0
  91. stouputils/data_science/models/keras/resnet.py +52 -0
  92. stouputils/data_science/models/keras/squeezenet.py +233 -0
  93. stouputils/data_science/models/keras/vgg.py +42 -0
  94. stouputils/data_science/models/keras/xception.py +38 -0
  95. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -0
  96. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -0
  97. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -0
  98. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -0
  99. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -0
  100. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -0
  101. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -0
  102. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -0
  103. stouputils/data_science/models/keras_utils/visualizations.py +416 -0
  104. stouputils/data_science/models/model_interface.py +939 -0
  105. stouputils/data_science/models/sandbox.py +116 -0
  106. stouputils/data_science/range_tuple.py +234 -0
  107. stouputils/data_science/scripts/augment_dataset.py +77 -0
  108. stouputils/data_science/scripts/exhaustive_process.py +133 -0
  109. stouputils/data_science/scripts/preprocess_dataset.py +70 -0
  110. stouputils/data_science/scripts/routine.py +168 -0
  111. stouputils/data_science/utils.py +285 -0
  112. stouputils/decorators.py +595 -0
  113. stouputils/decorators.pyi +242 -0
  114. stouputils/image.py +441 -0
  115. stouputils/image.pyi +172 -0
  116. stouputils/installer/__init__.py +18 -0
  117. stouputils/installer/__init__.pyi +5 -0
  118. stouputils/installer/common.py +67 -0
  119. stouputils/installer/common.pyi +39 -0
  120. stouputils/installer/downloader.py +101 -0
  121. stouputils/installer/downloader.pyi +24 -0
  122. stouputils/installer/linux.py +144 -0
  123. stouputils/installer/linux.pyi +39 -0
  124. stouputils/installer/main.py +223 -0
  125. stouputils/installer/main.pyi +57 -0
  126. stouputils/installer/windows.py +136 -0
  127. stouputils/installer/windows.pyi +31 -0
  128. stouputils/io.py +486 -0
  129. stouputils/io.pyi +213 -0
  130. stouputils/parallel.py +453 -0
  131. stouputils/parallel.pyi +211 -0
  132. stouputils/print.py +527 -0
  133. stouputils/print.pyi +146 -0
  134. stouputils/py.typed +1 -0
  135. stouputils-1.12.1.dist-info/METADATA +179 -0
  136. stouputils-1.12.1.dist-info/RECORD +138 -0
  137. stouputils-1.12.1.dist-info/WHEEL +4 -0
  138. stouputils-1.12.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,595 @@
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
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
380
+ # Create the cache dict
381
+ cache_dict: dict[bytes, Any] = {}
382
+
383
+ # Create the wrapper
384
+ @wraps(func)
385
+ def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
386
+
387
+ # Get the hashed key
388
+ if method == "str":
389
+ hashed: bytes = str(args).encode() + str(kwargs).encode()
390
+ elif method == "pickle":
391
+ hashed: bytes = pickle_dumps((args, kwargs))
392
+ else:
393
+ raise ValueError("Invalid caching method. Supported methods are 'str' and 'pickle'.")
394
+
395
+ # If the key is in the cache, return it
396
+ if hashed in cache_dict:
397
+ return cache_dict[hashed]
398
+
399
+ # Else, call the function and add the result to the cache
400
+ else:
401
+ result: Any = func(*args, **kwargs)
402
+ cache_dict[hashed] = result
403
+ return result
404
+
405
+ # Return the wrapper
406
+ wrapper.__name__ = _get_wrapper_name("stouputils.decorators.simple_cache", func)
407
+ return wrapper
408
+
409
+ # Handle both @simple_cache and @simple_cache(method=...)
410
+ if func is None:
411
+ return decorator
412
+ return decorator(func)
413
+
414
+ # Decorator that marks a function as abstract
415
+ def abstract(
416
+ func: Callable[..., Any] | None = None,
417
+ *,
418
+ error_log: LogLevels = LogLevels.RAISE_EXCEPTION
419
+ ) -> Callable[..., Any]:
420
+ """ Decorator that marks a function as abstract.
421
+
422
+ Contrary to the abstractmethod decorator from the abc module that raises a TypeError
423
+ when you try to instantiate a class that has abstract methods, this decorator raises
424
+ a NotImplementedError ONLY when the decorated function is called, indicating that the function
425
+ must be implemented by a subclass.
426
+
427
+ Args:
428
+ func (Callable[..., Any] | None): The function to mark as abstract
429
+ error_log (LogLevels): Log level for the error handling
430
+ LogLevels.NONE: None
431
+ LogLevels.WARNING: Show as warning
432
+ LogLevels.WARNING_TRACEBACK: Show as warning with traceback
433
+ LogLevels.ERROR_TRACEBACK: Show as error with traceback
434
+ LogLevels.RAISE_EXCEPTION: Raise exception
435
+
436
+ Returns:
437
+ Callable[..., Any]: Decorator that raises NotImplementedError when called
438
+
439
+ Examples:
440
+ >>> class Base:
441
+ ... @abstract
442
+ ... def method(self):
443
+ ... pass
444
+ >>> Base().method()
445
+ Traceback (most recent call last):
446
+ ...
447
+ NotImplementedError: Function 'method' is abstract and must be implemented by a subclass
448
+ """
449
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
450
+ message: str = f"Function '{_get_func_name(func)}' is abstract and must be implemented by a subclass"
451
+ if not func.__doc__:
452
+ func.__doc__ = message
453
+
454
+ @wraps(func)
455
+ @handle_error(exceptions=NotImplementedError, error_log=error_log)
456
+ def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
457
+ raise NotImplementedError(message)
458
+ wrapper.__name__ = _get_wrapper_name("stouputils.decorators.abstract", func)
459
+ return wrapper
460
+
461
+ # Handle both @abstract and @abstract(error_log=...)
462
+ if func is None:
463
+ return decorator
464
+ return decorator(func)
465
+
466
+ # Decorator that marks a function as deprecated
467
+ def deprecated(
468
+ func: Callable[..., Any] | None = None,
469
+ *,
470
+ message: str = "",
471
+ version: str = "",
472
+ error_log: LogLevels = LogLevels.WARNING
473
+ ) -> Callable[..., Any]:
474
+ """ Decorator that marks a function as deprecated.
475
+
476
+ Args:
477
+ func (Callable[..., Any] | None): Function to mark as deprecated
478
+ message (str): Additional message to display with the deprecation warning
479
+ version (str): Version since when the function is deprecated (e.g. "v1.2.0")
480
+ error_log (LogLevels): Log level for the deprecation warning
481
+ LogLevels.NONE: None
482
+ LogLevels.WARNING: Show as warning
483
+ LogLevels.WARNING_TRACEBACK: Show as warning with traceback
484
+ LogLevels.ERROR_TRACEBACK: Show as error with traceback
485
+ LogLevels.RAISE_EXCEPTION: Raise exception
486
+ Returns:
487
+ Callable[..., Any]: Decorator that marks a function as deprecated
488
+
489
+ Examples:
490
+ >>> @deprecated
491
+ ... def old_function():
492
+ ... pass
493
+
494
+ >>> @deprecated(message="Use 'new_function()' instead", error_log=LogLevels.WARNING)
495
+ ... def another_old_function():
496
+ ... pass
497
+ """
498
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
499
+ @wraps(func)
500
+ def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
501
+ # Build deprecation message
502
+ msg: str = f"Function '{_get_func_name(func)}()' is deprecated"
503
+ if version:
504
+ msg += f" since {version}"
505
+ if message:
506
+ msg += f". {message}"
507
+
508
+ # Handle deprecation warning based on log level
509
+ if error_log == LogLevels.WARNING:
510
+ warning(msg)
511
+ elif error_log == LogLevels.WARNING_TRACEBACK:
512
+ warning(f"{msg}\n{format_exc()}")
513
+ elif error_log == LogLevels.ERROR_TRACEBACK:
514
+ error(f"{msg}\n{format_exc()}", exit=True)
515
+ elif error_log == LogLevels.RAISE_EXCEPTION:
516
+ raise DeprecationWarning(msg)
517
+
518
+ # Call the original function
519
+ return func(*args, **kwargs)
520
+ wrapper.__name__ = _get_wrapper_name("stouputils.decorators.deprecated", func)
521
+ return wrapper
522
+
523
+ # Handle both @deprecated and @deprecated(message=..., error_log=...)
524
+ if func is None:
525
+ return decorator
526
+ return decorator(func)
527
+
528
+ # Decorator that make a function silent (disable stdout)
529
+ def silent(
530
+ func: Callable[..., Any] | None = None,
531
+ *,
532
+ mute_stderr: bool = False
533
+ ) -> Callable[..., Any]:
534
+ """ Decorator that makes a function silent (disable stdout, and stderr if specified).
535
+
536
+ Alternative to stouputils.ctx.Muffle.
537
+
538
+ Args:
539
+ func (Callable[..., Any] | None): Function to make silent
540
+ mute_stderr (bool): Whether to mute stderr or not
541
+
542
+ Examples:
543
+ >>> @silent
544
+ ... def test():
545
+ ... print("Hello, world!")
546
+ >>> test()
547
+
548
+ >>> @silent(mute_stderr=True)
549
+ ... def test2():
550
+ ... print("Hello, world!")
551
+ >>> test2()
552
+
553
+ >>> silent(print)("Hello, world!")
554
+ """
555
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
556
+ @wraps(func)
557
+ def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> Any:
558
+ # Use Muffle context manager to silence output
559
+ with Muffle(mute_stderr=mute_stderr):
560
+ return func(*args, **kwargs)
561
+ wrapper.__name__ = _get_wrapper_name("stouputils.decorators.silent", func)
562
+ return wrapper
563
+
564
+ # Handle both @silent and @silent(mute_stderr=...)
565
+ if func is None:
566
+ return decorator
567
+ return decorator(func)
568
+
569
+
570
+
571
+ # "Private" functions
572
+ def _get_func_name(func: Callable[..., Any]) -> str:
573
+ """ Get the name of a function, returns "<unknown>" if the name cannot be retrieved. """
574
+ try:
575
+ return func.__name__
576
+ except AttributeError:
577
+ return "<unknown>"
578
+
579
+ def _get_wrapper_name(decorator_name: str, func: Callable[..., Any]) -> str:
580
+ """ Get a descriptive name for a wrapper function.
581
+
582
+ Args:
583
+ decorator_name (str): Name of the decorator
584
+ func (Callable[..., Any]): Function being decorated
585
+ Returns:
586
+ str: Combined name for the wrapper function (e.g., "stouputils.decorators.handle_error@function_name")
587
+ """
588
+ func_name: str = _get_func_name(func)
589
+
590
+ # Remove "stouputils.decorators.*" prefix if present
591
+ if func_name.startswith("stouputils.decorators."):
592
+ func_name = ".".join(func_name.split(".")[3:])
593
+
594
+ return f"{decorator_name}@{func_name}"
595
+