winipedia-utils 0.2.0__py3-none-any.whl → 0.2.2__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 (90) hide show
  1. winipedia_utils/concurrent/concurrent.py +245 -245
  2. winipedia_utils/concurrent/multiprocessing.py +130 -130
  3. winipedia_utils/concurrent/multithreading.py +93 -93
  4. winipedia_utils/consts.py +21 -23
  5. winipedia_utils/data/__init__.py +1 -1
  6. winipedia_utils/data/dataframe/__init__.py +1 -1
  7. winipedia_utils/data/dataframe/cleaning.py +378 -378
  8. winipedia_utils/data/structures/__init__.py +1 -1
  9. winipedia_utils/data/structures/dicts.py +16 -16
  10. winipedia_utils/git/__init__.py +1 -1
  11. winipedia_utils/git/gitignore/__init__.py +1 -1
  12. winipedia_utils/git/gitignore/gitignore.py +136 -136
  13. winipedia_utils/git/pre_commit/__init__.py +1 -1
  14. winipedia_utils/git/pre_commit/config.py +70 -70
  15. winipedia_utils/git/pre_commit/hooks.py +109 -109
  16. winipedia_utils/git/pre_commit/run_hooks.py +49 -49
  17. winipedia_utils/iterating/__init__.py +1 -1
  18. winipedia_utils/iterating/iterate.py +29 -29
  19. winipedia_utils/logging/ansi.py +6 -6
  20. winipedia_utils/logging/config.py +64 -64
  21. winipedia_utils/logging/logger.py +26 -26
  22. winipedia_utils/modules/class_.py +119 -119
  23. winipedia_utils/modules/function.py +101 -101
  24. winipedia_utils/modules/module.py +379 -379
  25. winipedia_utils/modules/package.py +390 -390
  26. winipedia_utils/oop/mixins/meta.py +333 -333
  27. winipedia_utils/oop/mixins/mixin.py +37 -37
  28. winipedia_utils/os/__init__.py +1 -1
  29. winipedia_utils/os/os.py +63 -63
  30. winipedia_utils/projects/__init__.py +1 -1
  31. winipedia_utils/projects/poetry/__init__.py +1 -1
  32. winipedia_utils/projects/poetry/config.py +91 -91
  33. winipedia_utils/projects/poetry/poetry.py +31 -31
  34. winipedia_utils/projects/project.py +48 -48
  35. winipedia_utils/resources/__init__.py +1 -1
  36. winipedia_utils/resources/svgs/__init__.py +1 -1
  37. winipedia_utils/resources/svgs/download_arrow.svg +2 -2
  38. winipedia_utils/resources/svgs/exit_fullscreen_icon.svg +5 -5
  39. winipedia_utils/resources/svgs/fullscreen_icon.svg +2 -2
  40. winipedia_utils/resources/svgs/menu_icon.svg +3 -3
  41. winipedia_utils/resources/svgs/pause_icon.svg +3 -3
  42. winipedia_utils/resources/svgs/play_icon.svg +16 -16
  43. winipedia_utils/resources/svgs/plus_icon.svg +23 -23
  44. winipedia_utils/resources/svgs/svg.py +15 -15
  45. winipedia_utils/security/__init__.py +1 -1
  46. winipedia_utils/security/cryptography.py +29 -29
  47. winipedia_utils/security/keyring.py +70 -70
  48. winipedia_utils/setup.py +47 -47
  49. winipedia_utils/testing/assertions.py +23 -23
  50. winipedia_utils/testing/convention.py +177 -177
  51. winipedia_utils/testing/create_tests.py +291 -291
  52. winipedia_utils/testing/fixtures.py +28 -28
  53. winipedia_utils/testing/tests/base/fixtures/__init__.py +1 -1
  54. winipedia_utils/testing/tests/base/fixtures/fixture.py +6 -6
  55. winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +33 -33
  56. winipedia_utils/testing/tests/base/fixtures/scopes/function.py +7 -7
  57. winipedia_utils/testing/tests/base/fixtures/scopes/module.py +31 -31
  58. winipedia_utils/testing/tests/base/fixtures/scopes/package.py +7 -7
  59. winipedia_utils/testing/tests/base/fixtures/scopes/session.py +312 -312
  60. winipedia_utils/testing/tests/base/utils/utils.py +82 -82
  61. winipedia_utils/testing/tests/conftest.py +32 -32
  62. winipedia_utils/text/string.py +126 -126
  63. {winipedia_utils-0.2.0.dist-info → winipedia_utils-0.2.2.dist-info}/METADATA +3 -5
  64. winipedia_utils-0.2.2.dist-info/RECORD +80 -0
  65. {winipedia_utils-0.2.0.dist-info → winipedia_utils-0.2.2.dist-info}/licenses/LICENSE +21 -21
  66. winipedia_utils/django/__init__.py +0 -24
  67. winipedia_utils/django/bulk.py +0 -538
  68. winipedia_utils/django/command.py +0 -334
  69. winipedia_utils/django/database.py +0 -289
  70. winipedia_utils/pyside/__init__.py +0 -1
  71. winipedia_utils/pyside/core/__init__.py +0 -1
  72. winipedia_utils/pyside/core/py_qiodevice.py +0 -476
  73. winipedia_utils/pyside/ui/__init__.py +0 -1
  74. winipedia_utils/pyside/ui/base/__init__.py +0 -1
  75. winipedia_utils/pyside/ui/base/base.py +0 -180
  76. winipedia_utils/pyside/ui/pages/__init__.py +0 -1
  77. winipedia_utils/pyside/ui/pages/base/__init__.py +0 -1
  78. winipedia_utils/pyside/ui/pages/base/base.py +0 -92
  79. winipedia_utils/pyside/ui/pages/browser.py +0 -26
  80. winipedia_utils/pyside/ui/pages/player.py +0 -85
  81. winipedia_utils/pyside/ui/widgets/__init__.py +0 -1
  82. winipedia_utils/pyside/ui/widgets/browser.py +0 -243
  83. winipedia_utils/pyside/ui/widgets/clickable_widget.py +0 -57
  84. winipedia_utils/pyside/ui/widgets/media_player.py +0 -430
  85. winipedia_utils/pyside/ui/widgets/notification.py +0 -78
  86. winipedia_utils/pyside/ui/windows/__init__.py +0 -1
  87. winipedia_utils/pyside/ui/windows/base/__init__.py +0 -1
  88. winipedia_utils/pyside/ui/windows/base/base.py +0 -49
  89. winipedia_utils-0.2.0.dist-info/RECORD +0 -103
  90. {winipedia_utils-0.2.0.dist-info → winipedia_utils-0.2.2.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
+ """