ddutils 0.2.2__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.2 → ddutils-0.2.3}/PKG-INFO +46 -1
- {ddutils-0.2.2 → ddutils-0.2.3}/README.md +45 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/annotation_helpers.py +32 -1
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/class_helpers.py +50 -1
- {ddutils-0.2.2 → ddutils-0.2.3}/pyproject.toml +1 -1
- {ddutils-0.2.2 → ddutils-0.2.3}/pyproject.toml.orig +1 -1
- {ddutils-0.2.2 → ddutils-0.2.3}/LICENSE +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/__init__.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/convertors.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/datetime_helpers.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/function_exceptions_extractor.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/function_helpers.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/module_getter.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/object_getter.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/py.typed +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/safe_decorators.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.3}/ddutils/scoped_registry.py +0 -0
- {ddutils-0.2.2 → 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
|
|
@@ -216,6 +231,36 @@ get_generic_base_argument(UserRepository, Repository) # <class 'User'>
|
|
|
216
231
|
get_generic_base_argument(Repository, Repository) # None
|
|
217
232
|
```
|
|
218
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
|
+
|
|
219
264
|
## Function Helpers
|
|
220
265
|
|
|
221
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
|
|
@@ -198,6 +213,36 @@ get_generic_base_argument(UserRepository, Repository) # <class 'User'>
|
|
|
198
213
|
get_generic_base_argument(Repository, Repository) # None
|
|
199
214
|
```
|
|
200
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
|
+
|
|
201
246
|
## Function Helpers
|
|
202
247
|
|
|
203
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.', '')
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Callable, Sized
|
|
3
3
|
from typing import Any, TypeVar, get_args, get_origin
|
|
4
4
|
|
|
5
|
+
from ddutils.annotation_helpers import get_annotation_name
|
|
6
|
+
|
|
5
7
|
_T = TypeVar('_T', bound=Any)
|
|
6
8
|
|
|
7
9
|
|
|
@@ -45,6 +47,53 @@ def get_generic_base_argument(cls: type, generic_base: type, position: int = 0)
|
|
|
45
47
|
return None
|
|
46
48
|
|
|
47
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
|
+
|
|
48
97
|
class classproperty: # noqa: N801
|
|
49
98
|
"""
|
|
50
99
|
A descriptor that defines a read-only property at the class level.
|
|
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
|