ddutils 0.2.1__tar.gz → 0.2.3__tar.gz
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.
- {ddutils-0.2.1 → ddutils-0.2.3}/PKG-INFO +71 -1
- {ddutils-0.2.1 → ddutils-0.2.3}/README.md +70 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/annotation_helpers.py +32 -1
- ddutils-0.2.3/ddutils/class_helpers.py +136 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/pyproject.toml +1 -1
- {ddutils-0.2.1 → ddutils-0.2.3}/pyproject.toml.orig +1 -1
- ddutils-0.2.1/ddutils/class_helpers.py +0 -67
- {ddutils-0.2.1 → ddutils-0.2.3}/LICENSE +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/__init__.py +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/convertors.py +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/datetime_helpers.py +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/function_exceptions_extractor.py +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/function_helpers.py +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/module_getter.py +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/object_getter.py +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/py.typed +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/safe_decorators.py +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/scoped_registry.py +0 -0
- {ddutils-0.2.1 → ddutils-0.2.3}/ddutils/sequence_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddutils
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Domain Driven Utils Library
|
|
5
5
|
Keywords: python,ddutils
|
|
6
6
|
Author: davyddd
|
|
@@ -138,6 +138,21 @@ get_annotation_without_optional(Optional[int]) # <class 'int'>
|
|
|
138
138
|
get_annotation_without_optional(Optional[List[str]]) # typing.List[str]
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
### get_annotation_name
|
|
142
|
+
|
|
143
|
+
Returns a readable name of an annotation for error messages: a class, a union or a tuple of classes as
|
|
144
|
+
`isinstance` takes it.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from ddutils.annotation_helpers import get_annotation_name
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
get_annotation_name(int) # 'int'
|
|
151
|
+
get_annotation_name((int, float)) # 'int | float'
|
|
152
|
+
get_annotation_name(int | None) # 'int | None'
|
|
153
|
+
get_annotation_name(list[int]) # 'list[int]'
|
|
154
|
+
```
|
|
155
|
+
|
|
141
156
|
## Class Helpers
|
|
142
157
|
|
|
143
158
|
Helper functions for inspecting and extending class-level behavior
|
|
@@ -191,6 +206,61 @@ get_origin_class_of_method(C, 'method') # <class 'C'>
|
|
|
191
206
|
get_origin_class_of_method(B, 'method') # <class 'A'>
|
|
192
207
|
```
|
|
193
208
|
|
|
209
|
+
### get_generic_base_argument
|
|
210
|
+
|
|
211
|
+
Returns the type argument a class bound to a generic base when subclassing it. `None` when the base was not
|
|
212
|
+
subscripted, the `TypeVar` itself when the subclass is still generic.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from typing import Generic, TypeVar
|
|
216
|
+
|
|
217
|
+
from ddutils.class_helpers import get_generic_base_argument
|
|
218
|
+
|
|
219
|
+
T = TypeVar('T')
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class Repository(Generic[T]):
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class UserRepository(Repository[User]):
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
get_generic_base_argument(UserRepository, Repository) # <class 'User'>
|
|
231
|
+
get_generic_base_argument(Repository, Repository) # None
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### require_class_attribute
|
|
235
|
+
|
|
236
|
+
Returns a class attribute that subclasses are expected to declare, for `__init_subclass__` checks. A missing,
|
|
237
|
+
`None` or empty value raises `AttributeError`, a value of the wrong type raises `TypeError`; both name the
|
|
238
|
+
attribute and the class, so a misconfigured subclass fails at definition rather than on first use.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from typing import ClassVar
|
|
242
|
+
|
|
243
|
+
from ddutils.class_helpers import require_class_attribute
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class BaseConsumer:
|
|
247
|
+
topic: ClassVar[str]
|
|
248
|
+
batch_size: ClassVar[int] = 100
|
|
249
|
+
|
|
250
|
+
def __init_subclass__(cls, **kwargs):
|
|
251
|
+
super().__init_subclass__(**kwargs)
|
|
252
|
+
require_class_attribute(cls, 'topic', str)
|
|
253
|
+
require_class_attribute(cls, 'batch_size', int)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class EventConsumer(BaseConsumer):
|
|
257
|
+
topic = 'events'
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class BrokenConsumer(BaseConsumer): # AttributeError: `topic` class attribute of BrokenConsumer must be set
|
|
261
|
+
pass
|
|
262
|
+
```
|
|
263
|
+
|
|
194
264
|
## Function Helpers
|
|
195
265
|
|
|
196
266
|
Tools for creating and modifying Python functions at runtime.
|
|
@@ -120,6 +120,21 @@ get_annotation_without_optional(Optional[int]) # <class 'int'>
|
|
|
120
120
|
get_annotation_without_optional(Optional[List[str]]) # typing.List[str]
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
### get_annotation_name
|
|
124
|
+
|
|
125
|
+
Returns a readable name of an annotation for error messages: a class, a union or a tuple of classes as
|
|
126
|
+
`isinstance` takes it.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from ddutils.annotation_helpers import get_annotation_name
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
get_annotation_name(int) # 'int'
|
|
133
|
+
get_annotation_name((int, float)) # 'int | float'
|
|
134
|
+
get_annotation_name(int | None) # 'int | None'
|
|
135
|
+
get_annotation_name(list[int]) # 'list[int]'
|
|
136
|
+
```
|
|
137
|
+
|
|
123
138
|
## Class Helpers
|
|
124
139
|
|
|
125
140
|
Helper functions for inspecting and extending class-level behavior
|
|
@@ -173,6 +188,61 @@ get_origin_class_of_method(C, 'method') # <class 'C'>
|
|
|
173
188
|
get_origin_class_of_method(B, 'method') # <class 'A'>
|
|
174
189
|
```
|
|
175
190
|
|
|
191
|
+
### get_generic_base_argument
|
|
192
|
+
|
|
193
|
+
Returns the type argument a class bound to a generic base when subclassing it. `None` when the base was not
|
|
194
|
+
subscripted, the `TypeVar` itself when the subclass is still generic.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from typing import Generic, TypeVar
|
|
198
|
+
|
|
199
|
+
from ddutils.class_helpers import get_generic_base_argument
|
|
200
|
+
|
|
201
|
+
T = TypeVar('T')
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class Repository(Generic[T]):
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class UserRepository(Repository[User]):
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
get_generic_base_argument(UserRepository, Repository) # <class 'User'>
|
|
213
|
+
get_generic_base_argument(Repository, Repository) # None
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### require_class_attribute
|
|
217
|
+
|
|
218
|
+
Returns a class attribute that subclasses are expected to declare, for `__init_subclass__` checks. A missing,
|
|
219
|
+
`None` or empty value raises `AttributeError`, a value of the wrong type raises `TypeError`; both name the
|
|
220
|
+
attribute and the class, so a misconfigured subclass fails at definition rather than on first use.
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from typing import ClassVar
|
|
224
|
+
|
|
225
|
+
from ddutils.class_helpers import require_class_attribute
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class BaseConsumer:
|
|
229
|
+
topic: ClassVar[str]
|
|
230
|
+
batch_size: ClassVar[int] = 100
|
|
231
|
+
|
|
232
|
+
def __init_subclass__(cls, **kwargs):
|
|
233
|
+
super().__init_subclass__(**kwargs)
|
|
234
|
+
require_class_attribute(cls, 'topic', str)
|
|
235
|
+
require_class_attribute(cls, 'batch_size', int)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class EventConsumer(BaseConsumer):
|
|
239
|
+
topic = 'events'
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class BrokenConsumer(BaseConsumer): # AttributeError: `topic` class attribute of BrokenConsumer must be set
|
|
243
|
+
pass
|
|
244
|
+
```
|
|
245
|
+
|
|
176
246
|
## Function Helpers
|
|
177
247
|
|
|
178
248
|
Tools for creating and modifying Python functions at runtime.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from collections.abc import Sequence
|
|
3
3
|
from types import UnionType
|
|
4
|
-
from typing import Any, Union, get_args
|
|
4
|
+
from typing import Any, Union, get_args, get_origin
|
|
5
5
|
|
|
6
6
|
NON_COMPLEX_SEQUENCE_TYPES = (str, bytes, bytearray)
|
|
7
7
|
|
|
@@ -76,3 +76,34 @@ def get_annotation_without_optional(annotation: Any) -> Any:
|
|
|
76
76
|
raise TypeError('Union types are not supported') from None
|
|
77
77
|
|
|
78
78
|
return annotation_args[0]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _join_annotation_names(items: Any, separator: str) -> str:
|
|
82
|
+
return separator.join(get_annotation_name(item) for item in items)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_annotation_name(annotation: Any) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Return a readable name of an annotation for messages: `int`, `int | float`, `list[int]`, `None`.
|
|
88
|
+
|
|
89
|
+
Accepts a class, a union (`int | None`, `Optional[int]`, `Union[int, str]`), a generic (`dict[str, int]`,
|
|
90
|
+
`Literal['a', 'b']`) or a tuple of classes as `isinstance` takes it, and renders them the way they are written.
|
|
91
|
+
"""
|
|
92
|
+
if annotation is None or annotation is type(None):
|
|
93
|
+
return 'None'
|
|
94
|
+
if annotation is Ellipsis:
|
|
95
|
+
return '...'
|
|
96
|
+
if isinstance(annotation, (str, bytes, int, float)): # a `Literal` value
|
|
97
|
+
return repr(annotation)
|
|
98
|
+
if isinstance(annotation, tuple):
|
|
99
|
+
return _join_annotation_names(annotation, ' | ')
|
|
100
|
+
if isinstance(annotation, UnionType) or get_origin(annotation) is Union:
|
|
101
|
+
return _join_annotation_names(get_args(annotation), ' | ')
|
|
102
|
+
if isinstance(annotation, list): # the parameters of a `Callable`
|
|
103
|
+
return f'[{_join_annotation_names(annotation, ", ")}]'
|
|
104
|
+
origin, args = get_origin(annotation), get_args(annotation)
|
|
105
|
+
if origin is not None and args:
|
|
106
|
+
return f'{get_annotation_name(origin)}[{_join_annotation_names(args, ", ")}]'
|
|
107
|
+
if isinstance(annotation, type):
|
|
108
|
+
return annotation.__name__
|
|
109
|
+
return str(annotation).replace('typing.', '')
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable, Sized
|
|
3
|
+
from typing import Any, TypeVar, get_args, get_origin
|
|
4
|
+
|
|
5
|
+
from ddutils.annotation_helpers import get_annotation_name
|
|
6
|
+
|
|
7
|
+
_T = TypeVar('_T', bound=Any)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_origin_class_of_method(cls: Any, method_name: str) -> type | None:
|
|
11
|
+
"""
|
|
12
|
+
Find the class in the inheritance hierarchy where a method is originally defined.
|
|
13
|
+
|
|
14
|
+
This function traverses the Method Resolution Order (MRO) of the given class
|
|
15
|
+
to find the first class that defines the specified method in its own namespace.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
cls: The class to inspect.
|
|
19
|
+
method_name: The name of the method to locate.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The class in which the method is originally defined, or None if the method is not found.
|
|
23
|
+
"""
|
|
24
|
+
for base in inspect.getmro(cls):
|
|
25
|
+
if method_name in base.__dict__:
|
|
26
|
+
return base
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_generic_base_argument(cls: type, generic_base: type, position: int = 0) -> Any | None:
|
|
31
|
+
"""
|
|
32
|
+
Return the type argument that `cls` bound to `generic_base` when subclassing it.
|
|
33
|
+
|
|
34
|
+
For `class ProfileCache(GenericCache[Profile])` this is `Profile`. Returns `None` when `cls` does not
|
|
35
|
+
subscript `generic_base` (`class Cache(GenericCache)`), and a `TypeVar` when the subclass is still
|
|
36
|
+
generic (`class Base[T](GenericCache[T])`), so callers can reject both cases in `__init_subclass__`.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
cls: The class to inspect.
|
|
40
|
+
generic_base: The generic base class whose argument to look up.
|
|
41
|
+
position: Index of the argument for bases with several type parameters.
|
|
42
|
+
"""
|
|
43
|
+
for base in getattr(cls, '__orig_bases__', ()):
|
|
44
|
+
if get_origin(base) is generic_base:
|
|
45
|
+
arguments = get_args(base)
|
|
46
|
+
return arguments[position] if position < len(arguments) else None
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def require_class_attribute(
|
|
51
|
+
cls: type,
|
|
52
|
+
name: str,
|
|
53
|
+
expected_type: type | tuple[type, ...] | None = None,
|
|
54
|
+
*,
|
|
55
|
+
allow_empty: bool = False,
|
|
56
|
+
) -> Any:
|
|
57
|
+
"""
|
|
58
|
+
Return a class attribute that subclasses are expected to declare, for `__init_subclass__` checks.
|
|
59
|
+
|
|
60
|
+
A base class that reads its configuration from class attributes (`topic`, `bootstrap_servers`,
|
|
61
|
+
`serializer`, ...) wants a wrong or forgotten declaration to fail at class definition, with the
|
|
62
|
+
attribute and the class named, rather than deep inside the first call that uses it.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
cls: The class being defined, as passed to `__init_subclass__`.
|
|
66
|
+
name: The attribute name.
|
|
67
|
+
expected_type: A class or tuple of classes the value must be an instance of; `None` skips the check.
|
|
68
|
+
allow_empty: Accept an empty value (`''`, `[]`, `{}`, ...). Numbers are never "empty", so `0` passes.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
AttributeError: The attribute is missing or `None`, or empty unless `allow_empty`.
|
|
72
|
+
TypeError: The value is not an instance of `expected_type`.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
class BaseConsumer:
|
|
76
|
+
topic: ClassVar[str]
|
|
77
|
+
batch_size: ClassVar[int] = 100
|
|
78
|
+
|
|
79
|
+
def __init_subclass__(cls, **kwargs):
|
|
80
|
+
super().__init_subclass__(**kwargs)
|
|
81
|
+
require_class_attribute(cls, 'topic', str)
|
|
82
|
+
require_class_attribute(cls, 'batch_size', int)
|
|
83
|
+
"""
|
|
84
|
+
value = getattr(cls, name, None)
|
|
85
|
+
if value is None:
|
|
86
|
+
raise AttributeError(f'`{name}` class attribute of {cls.__name__} must be set')
|
|
87
|
+
if not allow_empty and isinstance(value, Sized) and len(value) == 0:
|
|
88
|
+
raise AttributeError(f'`{name}` class attribute of {cls.__name__} must not be empty')
|
|
89
|
+
if expected_type is not None and not isinstance(value, expected_type):
|
|
90
|
+
raise TypeError(
|
|
91
|
+
f'`{name}` class attribute of {cls.__name__} must be {get_annotation_name(expected_type)}, '
|
|
92
|
+
f'got {type(value).__name__}'
|
|
93
|
+
)
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class classproperty: # noqa: N801
|
|
98
|
+
"""
|
|
99
|
+
A descriptor that defines a read-only property at the class level.
|
|
100
|
+
|
|
101
|
+
Similar to the built-in @property decorator, but applies to the class itself rather than instances.
|
|
102
|
+
Allows access to computed values directly from the class without creating an instance.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
class MyClass:
|
|
106
|
+
@classproperty
|
|
107
|
+
def class_name(cls):
|
|
108
|
+
return cls.__name__
|
|
109
|
+
|
|
110
|
+
# Access without instantiation
|
|
111
|
+
name = MyClass.class_name
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(self, fget: Callable[..., Any]) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Initialize the classproperty with a class-level getter function.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
fget: A class-level getter function that takes the owning class as its only argument.
|
|
120
|
+
"""
|
|
121
|
+
self.fget = fget
|
|
122
|
+
self.__doc__ = fget.__doc__
|
|
123
|
+
self.__name__ = getattr(fget, '__name__', type(fget).__name__)
|
|
124
|
+
|
|
125
|
+
def __get__(self, instance: Any, owner: type[Any]) -> Any:
|
|
126
|
+
"""
|
|
127
|
+
Retrieve the property value using the descriptor protocol.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
instance: The instance accessing the property (can be None when accessed via the class).
|
|
131
|
+
owner: The class that owns this property.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The value returned by the getter function when called with the owner class.
|
|
135
|
+
"""
|
|
136
|
+
return self.fget(owner)
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
from collections.abc import Callable
|
|
3
|
-
from typing import Any, TypeVar
|
|
4
|
-
|
|
5
|
-
_T = TypeVar('_T', bound=Any)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def get_origin_class_of_method(cls: Any, method_name: str) -> type | None:
|
|
9
|
-
"""
|
|
10
|
-
Find the class in the inheritance hierarchy where a method is originally defined.
|
|
11
|
-
|
|
12
|
-
This function traverses the Method Resolution Order (MRO) of the given class
|
|
13
|
-
to find the first class that defines the specified method in its own namespace.
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
cls: The class to inspect.
|
|
17
|
-
method_name: The name of the method to locate.
|
|
18
|
-
|
|
19
|
-
Returns:
|
|
20
|
-
The class in which the method is originally defined, or None if the method is not found.
|
|
21
|
-
"""
|
|
22
|
-
for base in inspect.getmro(cls):
|
|
23
|
-
if method_name in base.__dict__:
|
|
24
|
-
return base
|
|
25
|
-
return None
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class classproperty: # noqa: N801
|
|
29
|
-
"""
|
|
30
|
-
A descriptor that defines a read-only property at the class level.
|
|
31
|
-
|
|
32
|
-
Similar to the built-in @property decorator, but applies to the class itself rather than instances.
|
|
33
|
-
Allows access to computed values directly from the class without creating an instance.
|
|
34
|
-
|
|
35
|
-
Example:
|
|
36
|
-
class MyClass:
|
|
37
|
-
@classproperty
|
|
38
|
-
def class_name(cls):
|
|
39
|
-
return cls.__name__
|
|
40
|
-
|
|
41
|
-
# Access without instantiation
|
|
42
|
-
name = MyClass.class_name
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
def __init__(self, fget: Callable[..., Any]) -> None:
|
|
46
|
-
"""
|
|
47
|
-
Initialize the classproperty with a class-level getter function.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
fget: A class-level getter function that takes the owning class as its only argument.
|
|
51
|
-
"""
|
|
52
|
-
self.fget = fget
|
|
53
|
-
self.__doc__ = fget.__doc__
|
|
54
|
-
self.__name__ = getattr(fget, '__name__', type(fget).__name__)
|
|
55
|
-
|
|
56
|
-
def __get__(self, instance: Any, owner: type[Any]) -> Any:
|
|
57
|
-
"""
|
|
58
|
-
Retrieve the property value using the descriptor protocol.
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
instance: The instance accessing the property (can be None when accessed via the class).
|
|
62
|
-
owner: The class that owns this property.
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
The value returned by the getter function when called with the owner class.
|
|
66
|
-
"""
|
|
67
|
-
return self.fget(owner)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|