winipedia-utils 0.2.10__py3-none-any.whl → 0.2.18__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.
Potentially problematic release.
This version of winipedia-utils might be problematic. Click here for more details.
- winipedia_utils/concurrent/concurrent.py +245 -245
- winipedia_utils/concurrent/multiprocessing.py +130 -130
- winipedia_utils/concurrent/multithreading.py +93 -93
- winipedia_utils/consts.py +21 -21
- winipedia_utils/data/__init__.py +1 -1
- winipedia_utils/data/dataframe/__init__.py +1 -1
- winipedia_utils/data/dataframe/cleaning.py +378 -378
- winipedia_utils/data/structures/__init__.py +1 -1
- winipedia_utils/data/structures/dicts.py +16 -16
- winipedia_utils/git/__init__.py +1 -1
- winipedia_utils/git/gitignore/__init__.py +1 -1
- winipedia_utils/git/gitignore/gitignore.py +136 -136
- winipedia_utils/git/pre_commit/__init__.py +1 -1
- winipedia_utils/git/pre_commit/config.py +70 -70
- winipedia_utils/git/pre_commit/hooks.py +127 -109
- winipedia_utils/git/pre_commit/run_hooks.py +49 -49
- winipedia_utils/iterating/__init__.py +1 -1
- winipedia_utils/iterating/iterate.py +29 -29
- winipedia_utils/logging/ansi.py +6 -6
- winipedia_utils/logging/config.py +64 -64
- winipedia_utils/logging/logger.py +26 -26
- winipedia_utils/modules/class_.py +119 -119
- winipedia_utils/modules/function.py +101 -101
- winipedia_utils/modules/module.py +379 -379
- winipedia_utils/modules/package.py +390 -390
- winipedia_utils/oop/mixins/meta.py +333 -333
- winipedia_utils/oop/mixins/mixin.py +37 -37
- winipedia_utils/os/__init__.py +1 -1
- winipedia_utils/os/os.py +63 -63
- winipedia_utils/projects/__init__.py +1 -1
- winipedia_utils/projects/poetry/__init__.py +1 -1
- winipedia_utils/projects/poetry/config.py +117 -117
- winipedia_utils/projects/poetry/poetry.py +31 -31
- winipedia_utils/projects/project.py +48 -48
- winipedia_utils/resources/__init__.py +1 -1
- winipedia_utils/resources/svgs/__init__.py +1 -1
- winipedia_utils/resources/svgs/download_arrow.svg +2 -2
- winipedia_utils/resources/svgs/exit_fullscreen_icon.svg +5 -5
- winipedia_utils/resources/svgs/fullscreen_icon.svg +2 -2
- winipedia_utils/resources/svgs/menu_icon.svg +3 -3
- winipedia_utils/resources/svgs/pause_icon.svg +3 -3
- winipedia_utils/resources/svgs/play_icon.svg +16 -16
- winipedia_utils/resources/svgs/plus_icon.svg +23 -23
- winipedia_utils/resources/svgs/svg.py +15 -15
- winipedia_utils/security/__init__.py +1 -1
- winipedia_utils/security/cryptography.py +29 -29
- winipedia_utils/security/keyring.py +70 -70
- winipedia_utils/setup.py +47 -47
- winipedia_utils/testing/assertions.py +23 -23
- winipedia_utils/testing/convention.py +177 -177
- winipedia_utils/testing/create_tests.py +297 -297
- winipedia_utils/testing/fixtures.py +28 -28
- winipedia_utils/testing/tests/base/fixtures/__init__.py +1 -1
- winipedia_utils/testing/tests/base/fixtures/fixture.py +6 -6
- winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +33 -33
- winipedia_utils/testing/tests/base/fixtures/scopes/function.py +7 -7
- winipedia_utils/testing/tests/base/fixtures/scopes/module.py +33 -33
- winipedia_utils/testing/tests/base/fixtures/scopes/package.py +7 -7
- winipedia_utils/testing/tests/base/fixtures/scopes/session.py +296 -296
- winipedia_utils/testing/tests/base/utils/utils.py +111 -111
- winipedia_utils/testing/tests/conftest.py +32 -32
- winipedia_utils/text/string.py +126 -126
- winipedia_utils-0.2.18.dist-info/METADATA +715 -0
- winipedia_utils-0.2.18.dist-info/RECORD +80 -0
- {winipedia_utils-0.2.10.dist-info → winipedia_utils-0.2.18.dist-info}/licenses/LICENSE +21 -21
- winipedia_utils/testing/tests/test_0.py +0 -8
- winipedia_utils-0.2.10.dist-info/METADATA +0 -355
- winipedia_utils-0.2.10.dist-info/RECORD +0 -81
- {winipedia_utils-0.2.10.dist-info → winipedia_utils-0.2.18.dist-info}/WHEEL +0 -0
|
@@ -1,333 +1,333 @@
|
|
|
1
|
-
"""Metaclass utilities for class behavior modification and enforcement.
|
|
2
|
-
|
|
3
|
-
This module provides metaclasses that can be used to
|
|
4
|
-
modify class behavior at creation time.
|
|
5
|
-
These metaclasses can be used individually or combined to create classes
|
|
6
|
-
with enhanced capabilities and stricter implementation requirements.
|
|
7
|
-
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import time
|
|
11
|
-
from abc import ABCMeta, abstractmethod
|
|
12
|
-
from collections.abc import Callable
|
|
13
|
-
from functools import wraps
|
|
14
|
-
from typing import Any, final
|
|
15
|
-
|
|
16
|
-
from winipedia_utils.logging.logger import get_logger
|
|
17
|
-
from winipedia_utils.modules.class_ import get_all_methods_from_cls
|
|
18
|
-
from winipedia_utils.modules.function import is_func, unwrap_method
|
|
19
|
-
from winipedia_utils.text.string import value_to_truncated_string
|
|
20
|
-
|
|
21
|
-
logger = get_logger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ABCLoggingMeta(ABCMeta):
|
|
25
|
-
"""Metaclass that automatically adds logging to class methods.
|
|
26
|
-
|
|
27
|
-
Wraps non-magic methods with a logging decorator that tracks method calls,
|
|
28
|
-
arguments, execution time, and return values. Includes rate limiting to
|
|
29
|
-
prevent log flooding.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def __new__(
|
|
33
|
-
mcs: type["ABCLoggingMeta"],
|
|
34
|
-
name: str,
|
|
35
|
-
bases: tuple[type, ...],
|
|
36
|
-
dct: dict[str, Any],
|
|
37
|
-
) -> "ABCLoggingMeta":
|
|
38
|
-
"""Create a new class with logging-wrapped methods.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
mcs: The metaclass instance
|
|
42
|
-
name: The name of the class being created
|
|
43
|
-
bases: The base classes of the class being created
|
|
44
|
-
dct: The attribute dictionary of the class being created
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
A new class with logging functionality added to its methods
|
|
48
|
-
|
|
49
|
-
"""
|
|
50
|
-
# Wrap all callables of the class with a logging wrapper
|
|
51
|
-
|
|
52
|
-
for attr_name, attr_value in dct.items():
|
|
53
|
-
if mcs.is_loggable_method(attr_value):
|
|
54
|
-
if isinstance(attr_value, classmethod):
|
|
55
|
-
wrapped_method = mcs.wrap_with_logging(
|
|
56
|
-
func=attr_value.__func__, class_name=name, call_times={}
|
|
57
|
-
)
|
|
58
|
-
dct[attr_name] = classmethod(wrapped_method)
|
|
59
|
-
elif isinstance(attr_value, staticmethod):
|
|
60
|
-
wrapped_method = mcs.wrap_with_logging(
|
|
61
|
-
func=attr_value.__func__, class_name=name, call_times={}
|
|
62
|
-
)
|
|
63
|
-
dct[attr_name] = staticmethod(wrapped_method)
|
|
64
|
-
else:
|
|
65
|
-
dct[attr_name] = mcs.wrap_with_logging(
|
|
66
|
-
func=attr_value, class_name=name, call_times={}
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return super().__new__(mcs, name, bases, dct)
|
|
70
|
-
|
|
71
|
-
@staticmethod
|
|
72
|
-
def is_loggable_method(method: Callable[..., Any]) -> bool:
|
|
73
|
-
"""Determine if a method should have logging applied.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
method: The method to check, properties are not logged
|
|
77
|
-
as they are not callable and it turns out to be tricky with them
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
True if the method should be wrapped with logging, False otherwise
|
|
81
|
-
|
|
82
|
-
"""
|
|
83
|
-
return (
|
|
84
|
-
is_func(method) # must be a method-like attribute
|
|
85
|
-
and hasattr(method, "__name__") # must have a name
|
|
86
|
-
and not method.__name__.startswith("__") # must not be a magic method
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
@staticmethod
|
|
90
|
-
def wrap_with_logging(
|
|
91
|
-
func: Callable[..., Any],
|
|
92
|
-
class_name: str,
|
|
93
|
-
call_times: dict[str, float],
|
|
94
|
-
) -> Callable[..., Any]:
|
|
95
|
-
"""Wrap a function with logging functionality.
|
|
96
|
-
|
|
97
|
-
Creates a wrapper that logs method calls, arguments, execution time,
|
|
98
|
-
and return values. Includes rate limiting to prevent excessive logging.
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
func: The function to wrap with logging
|
|
102
|
-
class_name: The name of the class containing the function
|
|
103
|
-
call_times: Dictionary to track when methods were last called
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
A wrapped function with logging capabilities
|
|
107
|
-
|
|
108
|
-
"""
|
|
109
|
-
time_time = time.time # Cache the time.time function for performance
|
|
110
|
-
|
|
111
|
-
@wraps(func)
|
|
112
|
-
def wrapper(*args: object, **kwargs: object) -> object:
|
|
113
|
-
# call_times as a dictionary to store the call times of the function
|
|
114
|
-
# we only log if the time since the last call is greater than the threshold
|
|
115
|
-
# this is to avoid spamming the logs
|
|
116
|
-
|
|
117
|
-
func_name = func.__name__
|
|
118
|
-
|
|
119
|
-
threshold = 1
|
|
120
|
-
|
|
121
|
-
last_call_time = call_times.get(func_name, 0)
|
|
122
|
-
|
|
123
|
-
current_time = time_time()
|
|
124
|
-
|
|
125
|
-
do_logging = (current_time - last_call_time) > threshold
|
|
126
|
-
|
|
127
|
-
max_log_length = 20
|
|
128
|
-
|
|
129
|
-
if do_logging:
|
|
130
|
-
args_str = value_to_truncated_string(
|
|
131
|
-
value=args, max_length=max_log_length
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
kwargs_str = value_to_truncated_string(
|
|
135
|
-
value=kwargs, max_length=max_log_length
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
logger.info(
|
|
139
|
-
"%s - Calling %s with %s and %s",
|
|
140
|
-
class_name,
|
|
141
|
-
func_name,
|
|
142
|
-
args_str,
|
|
143
|
-
kwargs_str,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# Execute the function and return the result
|
|
147
|
-
|
|
148
|
-
result = func(*args, **kwargs)
|
|
149
|
-
|
|
150
|
-
if do_logging:
|
|
151
|
-
duration = time_time() - current_time
|
|
152
|
-
|
|
153
|
-
result_str = value_to_truncated_string(
|
|
154
|
-
value=result, max_length=max_log_length
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
logger.info(
|
|
158
|
-
"%s - %s finished with %s seconds -> returning %s",
|
|
159
|
-
class_name,
|
|
160
|
-
func_name,
|
|
161
|
-
duration,
|
|
162
|
-
result_str,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
# save the call time for the next call
|
|
166
|
-
|
|
167
|
-
call_times[func_name] = current_time
|
|
168
|
-
|
|
169
|
-
return result
|
|
170
|
-
|
|
171
|
-
return wrapper
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
class StrictABCMeta(ABCMeta):
|
|
175
|
-
"""Metaclass that enforces implementation.
|
|
176
|
-
|
|
177
|
-
Ensures that concrete subclasses properly implement all required attributes
|
|
178
|
-
and that their types match the expected types from type annotations.
|
|
179
|
-
Additionally enforces that methods must be decorated with either @final or
|
|
180
|
-
@abstractmethod to make design intentions explicit.
|
|
181
|
-
"""
|
|
182
|
-
|
|
183
|
-
def __init__(
|
|
184
|
-
cls: "StrictABCMeta",
|
|
185
|
-
name: str,
|
|
186
|
-
bases: tuple[type, ...],
|
|
187
|
-
namespace: dict[str, Any],
|
|
188
|
-
/,
|
|
189
|
-
**_kwargs: Any,
|
|
190
|
-
) -> None:
|
|
191
|
-
"""Initialize a class with implementation checking.
|
|
192
|
-
|
|
193
|
-
Verifies that concrete classes (non-abstract) properly implement
|
|
194
|
-
all required attributes with the correct types. Also checks that
|
|
195
|
-
methods are properly decorated with @final or @abstractmethod.
|
|
196
|
-
|
|
197
|
-
Args:
|
|
198
|
-
cls: The class being initialized
|
|
199
|
-
name: The name of the class
|
|
200
|
-
bases: The base classes
|
|
201
|
-
namespace: The attribute dictionary
|
|
202
|
-
|
|
203
|
-
Raises:
|
|
204
|
-
NotImplementedError: If the class doesn't define __abstract__
|
|
205
|
-
ValueError: If a required attribute is not implemented
|
|
206
|
-
TypeError: If an implemented attribute has the wrong type
|
|
207
|
-
TypeError: If a method is neither final nor abstract
|
|
208
|
-
|
|
209
|
-
"""
|
|
210
|
-
super().__init__(name, bases, namespace)
|
|
211
|
-
|
|
212
|
-
# Check method decorators regardless of abstract status
|
|
213
|
-
|
|
214
|
-
cls.check_method_decorators()
|
|
215
|
-
|
|
216
|
-
if cls.is_abstract_cls():
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
cls.check_attrs_implemented()
|
|
220
|
-
|
|
221
|
-
def is_abstract_cls(cls) -> bool:
|
|
222
|
-
"""Check if the class is abstract.
|
|
223
|
-
|
|
224
|
-
Determines abstractness based on if any methods have @abstractmethod.
|
|
225
|
-
|
|
226
|
-
Returns:
|
|
227
|
-
True if the class is abstract, False otherwise
|
|
228
|
-
|
|
229
|
-
"""
|
|
230
|
-
return any(cls.__abstractmethods__)
|
|
231
|
-
|
|
232
|
-
def check_method_decorators(cls) -> None:
|
|
233
|
-
"""Check that all methods are properly decorated with @final or @abstractmethod.
|
|
234
|
-
|
|
235
|
-
Verifies that all methods in the class are explicitly marked
|
|
236
|
-
as either final or abstract to enforce design intentions.
|
|
237
|
-
|
|
238
|
-
Raises:
|
|
239
|
-
TypeError: If a method is neither final nor abstract
|
|
240
|
-
|
|
241
|
-
"""
|
|
242
|
-
# Get all methods defined in this class (not inherited)
|
|
243
|
-
|
|
244
|
-
for func in get_all_methods_from_cls(cls, exclude_parent_methods=True):
|
|
245
|
-
# Check if the method is marked as final or abstract
|
|
246
|
-
|
|
247
|
-
if not cls.is_final_method(func) and not cls.is_abstract_method(func):
|
|
248
|
-
msg = (
|
|
249
|
-
f"Method {cls.__name__}.{func.__name__} must be decorated with "
|
|
250
|
-
f"@{final.__name__} or @{abstractmethod.__name__} "
|
|
251
|
-
f"to make design intentions explicit."
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
raise TypeError(msg)
|
|
255
|
-
|
|
256
|
-
@staticmethod
|
|
257
|
-
def is_final_method(method: Callable[..., Any]) -> bool:
|
|
258
|
-
"""Check if a method is marked as final.
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
method: The method to check
|
|
262
|
-
|
|
263
|
-
Returns:
|
|
264
|
-
True if the method is marked with @final, False otherwise
|
|
265
|
-
|
|
266
|
-
"""
|
|
267
|
-
unwrapped_method = unwrap_method(method)
|
|
268
|
-
return getattr(method, "__final__", False) or getattr(
|
|
269
|
-
unwrapped_method, "__final__", False
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
@staticmethod
|
|
273
|
-
def is_abstract_method(method: Callable[..., Any]) -> bool:
|
|
274
|
-
"""Check if a method is an abstract method.
|
|
275
|
-
|
|
276
|
-
Args:
|
|
277
|
-
method: The method to check
|
|
278
|
-
|
|
279
|
-
Returns:
|
|
280
|
-
True if the method is marked with @abstractmethod, False otherwise
|
|
281
|
-
|
|
282
|
-
"""
|
|
283
|
-
return getattr(method, "__isabstractmethod__", False)
|
|
284
|
-
|
|
285
|
-
def check_attrs_implemented(cls) -> None:
|
|
286
|
-
"""Check that all required attributes are implemented.
|
|
287
|
-
|
|
288
|
-
Verifies that all attributes marked as NotImplemented in parent classes
|
|
289
|
-
are properly implemented in this class, and that their types match
|
|
290
|
-
the expected types from type annotations.
|
|
291
|
-
|
|
292
|
-
Raises:
|
|
293
|
-
ValueError: If a required attribute is not implemented
|
|
294
|
-
|
|
295
|
-
"""
|
|
296
|
-
for attr in cls.attrs_to_implement():
|
|
297
|
-
value = getattr(cls, attr, NotImplemented)
|
|
298
|
-
|
|
299
|
-
if value is NotImplemented:
|
|
300
|
-
msg = f"{attr=} must be implemented."
|
|
301
|
-
|
|
302
|
-
raise ValueError(msg)
|
|
303
|
-
|
|
304
|
-
def attrs_to_implement(cls) -> list[str]:
|
|
305
|
-
"""Find all attributes marked as NotImplemented in parent classes.
|
|
306
|
-
|
|
307
|
-
Searches the class hierarchy for attributes that are set to NotImplemented,
|
|
308
|
-
which indicates they must be implemented by concrete subclasses.
|
|
309
|
-
|
|
310
|
-
Returns:
|
|
311
|
-
List of attribute names that must be implemented
|
|
312
|
-
|
|
313
|
-
"""
|
|
314
|
-
attrs = {
|
|
315
|
-
attr
|
|
316
|
-
for base_class in cls.__mro__
|
|
317
|
-
for attr in dir(base_class)
|
|
318
|
-
if getattr(base_class, attr, None) is NotImplemented
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return list(attrs)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
class StrictABCLoggingMeta(StrictABCMeta, ABCLoggingMeta):
|
|
325
|
-
"""Combined metaclass that merges implementation, logging, and ABC functionality.
|
|
326
|
-
|
|
327
|
-
This metaclass combines the features of:
|
|
328
|
-
- ImplementationMeta: Enforces implementation of required attributes
|
|
329
|
-
- LoggingMeta: Adds automatic logging to methods
|
|
330
|
-
- ABCMeta: Provides abstract base class functionality
|
|
331
|
-
|
|
332
|
-
Use this metaclass when you need all three behaviors in a single class.
|
|
333
|
-
"""
|
|
1
|
+
"""Metaclass utilities for class behavior modification and enforcement.
|
|
2
|
+
|
|
3
|
+
This module provides metaclasses that can be used to
|
|
4
|
+
modify class behavior at creation time.
|
|
5
|
+
These metaclasses can be used individually or combined to create classes
|
|
6
|
+
with enhanced capabilities and stricter implementation requirements.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from abc import ABCMeta, abstractmethod
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from functools import wraps
|
|
14
|
+
from typing import Any, final
|
|
15
|
+
|
|
16
|
+
from winipedia_utils.logging.logger import get_logger
|
|
17
|
+
from winipedia_utils.modules.class_ import get_all_methods_from_cls
|
|
18
|
+
from winipedia_utils.modules.function import is_func, unwrap_method
|
|
19
|
+
from winipedia_utils.text.string import value_to_truncated_string
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ABCLoggingMeta(ABCMeta):
|
|
25
|
+
"""Metaclass that automatically adds logging to class methods.
|
|
26
|
+
|
|
27
|
+
Wraps non-magic methods with a logging decorator that tracks method calls,
|
|
28
|
+
arguments, execution time, and return values. Includes rate limiting to
|
|
29
|
+
prevent log flooding.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __new__(
|
|
33
|
+
mcs: type["ABCLoggingMeta"],
|
|
34
|
+
name: str,
|
|
35
|
+
bases: tuple[type, ...],
|
|
36
|
+
dct: dict[str, Any],
|
|
37
|
+
) -> "ABCLoggingMeta":
|
|
38
|
+
"""Create a new class with logging-wrapped methods.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
mcs: The metaclass instance
|
|
42
|
+
name: The name of the class being created
|
|
43
|
+
bases: The base classes of the class being created
|
|
44
|
+
dct: The attribute dictionary of the class being created
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A new class with logging functionality added to its methods
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
# Wrap all callables of the class with a logging wrapper
|
|
51
|
+
|
|
52
|
+
for attr_name, attr_value in dct.items():
|
|
53
|
+
if mcs.is_loggable_method(attr_value):
|
|
54
|
+
if isinstance(attr_value, classmethod):
|
|
55
|
+
wrapped_method = mcs.wrap_with_logging(
|
|
56
|
+
func=attr_value.__func__, class_name=name, call_times={}
|
|
57
|
+
)
|
|
58
|
+
dct[attr_name] = classmethod(wrapped_method)
|
|
59
|
+
elif isinstance(attr_value, staticmethod):
|
|
60
|
+
wrapped_method = mcs.wrap_with_logging(
|
|
61
|
+
func=attr_value.__func__, class_name=name, call_times={}
|
|
62
|
+
)
|
|
63
|
+
dct[attr_name] = staticmethod(wrapped_method)
|
|
64
|
+
else:
|
|
65
|
+
dct[attr_name] = mcs.wrap_with_logging(
|
|
66
|
+
func=attr_value, class_name=name, call_times={}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return super().__new__(mcs, name, bases, dct)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def is_loggable_method(method: Callable[..., Any]) -> bool:
|
|
73
|
+
"""Determine if a method should have logging applied.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
method: The method to check, properties are not logged
|
|
77
|
+
as they are not callable and it turns out to be tricky with them
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if the method should be wrapped with logging, False otherwise
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
return (
|
|
84
|
+
is_func(method) # must be a method-like attribute
|
|
85
|
+
and hasattr(method, "__name__") # must have a name
|
|
86
|
+
and not method.__name__.startswith("__") # must not be a magic method
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def wrap_with_logging(
|
|
91
|
+
func: Callable[..., Any],
|
|
92
|
+
class_name: str,
|
|
93
|
+
call_times: dict[str, float],
|
|
94
|
+
) -> Callable[..., Any]:
|
|
95
|
+
"""Wrap a function with logging functionality.
|
|
96
|
+
|
|
97
|
+
Creates a wrapper that logs method calls, arguments, execution time,
|
|
98
|
+
and return values. Includes rate limiting to prevent excessive logging.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
func: The function to wrap with logging
|
|
102
|
+
class_name: The name of the class containing the function
|
|
103
|
+
call_times: Dictionary to track when methods were last called
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A wrapped function with logging capabilities
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
time_time = time.time # Cache the time.time function for performance
|
|
110
|
+
|
|
111
|
+
@wraps(func)
|
|
112
|
+
def wrapper(*args: object, **kwargs: object) -> object:
|
|
113
|
+
# call_times as a dictionary to store the call times of the function
|
|
114
|
+
# we only log if the time since the last call is greater than the threshold
|
|
115
|
+
# this is to avoid spamming the logs
|
|
116
|
+
|
|
117
|
+
func_name = func.__name__
|
|
118
|
+
|
|
119
|
+
threshold = 1
|
|
120
|
+
|
|
121
|
+
last_call_time = call_times.get(func_name, 0)
|
|
122
|
+
|
|
123
|
+
current_time = time_time()
|
|
124
|
+
|
|
125
|
+
do_logging = (current_time - last_call_time) > threshold
|
|
126
|
+
|
|
127
|
+
max_log_length = 20
|
|
128
|
+
|
|
129
|
+
if do_logging:
|
|
130
|
+
args_str = value_to_truncated_string(
|
|
131
|
+
value=args, max_length=max_log_length
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
kwargs_str = value_to_truncated_string(
|
|
135
|
+
value=kwargs, max_length=max_log_length
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
logger.info(
|
|
139
|
+
"%s - Calling %s with %s and %s",
|
|
140
|
+
class_name,
|
|
141
|
+
func_name,
|
|
142
|
+
args_str,
|
|
143
|
+
kwargs_str,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Execute the function and return the result
|
|
147
|
+
|
|
148
|
+
result = func(*args, **kwargs)
|
|
149
|
+
|
|
150
|
+
if do_logging:
|
|
151
|
+
duration = time_time() - current_time
|
|
152
|
+
|
|
153
|
+
result_str = value_to_truncated_string(
|
|
154
|
+
value=result, max_length=max_log_length
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
logger.info(
|
|
158
|
+
"%s - %s finished with %s seconds -> returning %s",
|
|
159
|
+
class_name,
|
|
160
|
+
func_name,
|
|
161
|
+
duration,
|
|
162
|
+
result_str,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# save the call time for the next call
|
|
166
|
+
|
|
167
|
+
call_times[func_name] = current_time
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
return wrapper
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class StrictABCMeta(ABCMeta):
|
|
175
|
+
"""Metaclass that enforces implementation.
|
|
176
|
+
|
|
177
|
+
Ensures that concrete subclasses properly implement all required attributes
|
|
178
|
+
and that their types match the expected types from type annotations.
|
|
179
|
+
Additionally enforces that methods must be decorated with either @final or
|
|
180
|
+
@abstractmethod to make design intentions explicit.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(
|
|
184
|
+
cls: "StrictABCMeta",
|
|
185
|
+
name: str,
|
|
186
|
+
bases: tuple[type, ...],
|
|
187
|
+
namespace: dict[str, Any],
|
|
188
|
+
/,
|
|
189
|
+
**_kwargs: Any,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Initialize a class with implementation checking.
|
|
192
|
+
|
|
193
|
+
Verifies that concrete classes (non-abstract) properly implement
|
|
194
|
+
all required attributes with the correct types. Also checks that
|
|
195
|
+
methods are properly decorated with @final or @abstractmethod.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
cls: The class being initialized
|
|
199
|
+
name: The name of the class
|
|
200
|
+
bases: The base classes
|
|
201
|
+
namespace: The attribute dictionary
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
NotImplementedError: If the class doesn't define __abstract__
|
|
205
|
+
ValueError: If a required attribute is not implemented
|
|
206
|
+
TypeError: If an implemented attribute has the wrong type
|
|
207
|
+
TypeError: If a method is neither final nor abstract
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
super().__init__(name, bases, namespace)
|
|
211
|
+
|
|
212
|
+
# Check method decorators regardless of abstract status
|
|
213
|
+
|
|
214
|
+
cls.check_method_decorators()
|
|
215
|
+
|
|
216
|
+
if cls.is_abstract_cls():
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
cls.check_attrs_implemented()
|
|
220
|
+
|
|
221
|
+
def is_abstract_cls(cls) -> bool:
|
|
222
|
+
"""Check if the class is abstract.
|
|
223
|
+
|
|
224
|
+
Determines abstractness based on if any methods have @abstractmethod.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
True if the class is abstract, False otherwise
|
|
228
|
+
|
|
229
|
+
"""
|
|
230
|
+
return any(cls.__abstractmethods__)
|
|
231
|
+
|
|
232
|
+
def check_method_decorators(cls) -> None:
|
|
233
|
+
"""Check that all methods are properly decorated with @final or @abstractmethod.
|
|
234
|
+
|
|
235
|
+
Verifies that all methods in the class are explicitly marked
|
|
236
|
+
as either final or abstract to enforce design intentions.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
TypeError: If a method is neither final nor abstract
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
# Get all methods defined in this class (not inherited)
|
|
243
|
+
|
|
244
|
+
for func in get_all_methods_from_cls(cls, exclude_parent_methods=True):
|
|
245
|
+
# Check if the method is marked as final or abstract
|
|
246
|
+
|
|
247
|
+
if not cls.is_final_method(func) and not cls.is_abstract_method(func):
|
|
248
|
+
msg = (
|
|
249
|
+
f"Method {cls.__name__}.{func.__name__} must be decorated with "
|
|
250
|
+
f"@{final.__name__} or @{abstractmethod.__name__} "
|
|
251
|
+
f"to make design intentions explicit."
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
raise TypeError(msg)
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def is_final_method(method: Callable[..., Any]) -> bool:
|
|
258
|
+
"""Check if a method is marked as final.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
method: The method to check
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
True if the method is marked with @final, False otherwise
|
|
265
|
+
|
|
266
|
+
"""
|
|
267
|
+
unwrapped_method = unwrap_method(method)
|
|
268
|
+
return getattr(method, "__final__", False) or getattr(
|
|
269
|
+
unwrapped_method, "__final__", False
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def is_abstract_method(method: Callable[..., Any]) -> bool:
|
|
274
|
+
"""Check if a method is an abstract method.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
method: The method to check
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
True if the method is marked with @abstractmethod, False otherwise
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
return getattr(method, "__isabstractmethod__", False)
|
|
284
|
+
|
|
285
|
+
def check_attrs_implemented(cls) -> None:
|
|
286
|
+
"""Check that all required attributes are implemented.
|
|
287
|
+
|
|
288
|
+
Verifies that all attributes marked as NotImplemented in parent classes
|
|
289
|
+
are properly implemented in this class, and that their types match
|
|
290
|
+
the expected types from type annotations.
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
ValueError: If a required attribute is not implemented
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
for attr in cls.attrs_to_implement():
|
|
297
|
+
value = getattr(cls, attr, NotImplemented)
|
|
298
|
+
|
|
299
|
+
if value is NotImplemented:
|
|
300
|
+
msg = f"{attr=} must be implemented."
|
|
301
|
+
|
|
302
|
+
raise ValueError(msg)
|
|
303
|
+
|
|
304
|
+
def attrs_to_implement(cls) -> list[str]:
|
|
305
|
+
"""Find all attributes marked as NotImplemented in parent classes.
|
|
306
|
+
|
|
307
|
+
Searches the class hierarchy for attributes that are set to NotImplemented,
|
|
308
|
+
which indicates they must be implemented by concrete subclasses.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
List of attribute names that must be implemented
|
|
312
|
+
|
|
313
|
+
"""
|
|
314
|
+
attrs = {
|
|
315
|
+
attr
|
|
316
|
+
for base_class in cls.__mro__
|
|
317
|
+
for attr in dir(base_class)
|
|
318
|
+
if getattr(base_class, attr, None) is NotImplemented
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return list(attrs)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class StrictABCLoggingMeta(StrictABCMeta, ABCLoggingMeta):
|
|
325
|
+
"""Combined metaclass that merges implementation, logging, and ABC functionality.
|
|
326
|
+
|
|
327
|
+
This metaclass combines the features of:
|
|
328
|
+
- ImplementationMeta: Enforces implementation of required attributes
|
|
329
|
+
- LoggingMeta: Adds automatic logging to methods
|
|
330
|
+
- ABCMeta: Provides abstract base class functionality
|
|
331
|
+
|
|
332
|
+
Use this metaclass when you need all three behaviors in a single class.
|
|
333
|
+
"""
|