ddutils 0.2.2__tar.gz → 0.2.4__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.4}/PKG-INFO +122 -2
- {ddutils-0.2.2 → ddutils-0.2.4}/README.md +121 -1
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/annotation_helpers.py +33 -2
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/class_helpers.py +50 -1
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/convertors.py +12 -1
- ddutils-0.2.4/ddutils/value_helpers.py +30 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/pyproject.toml +1 -1
- {ddutils-0.2.2 → ddutils-0.2.4}/pyproject.toml.orig +1 -1
- {ddutils-0.2.2 → ddutils-0.2.4}/LICENSE +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/__init__.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/datetime_helpers.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/function_exceptions_extractor.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/function_helpers.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/module_getter.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/object_getter.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/py.typed +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/safe_decorators.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/ddutils/scoped_registry.py +0 -0
- {ddutils-0.2.2 → ddutils-0.2.4}/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.4
|
|
4
4
|
Summary: Domain Driven Utils Library
|
|
5
5
|
Keywords: python,ddutils
|
|
6
6
|
Author: davyddd
|
|
@@ -84,16 +84,23 @@ is_subclass(List[str], dict) # False
|
|
|
84
84
|
|
|
85
85
|
### is_complex_sequence
|
|
86
86
|
|
|
87
|
-
Checks whether a type annotation represents a sequence type, excluding `str`, `bytes`, and `bytearray
|
|
87
|
+
Checks whether a type annotation represents a sequence type, excluding `str`, `bytes`, and `bytearray`
|
|
88
|
+
and their subclasses (a `str`-based `Enum` is not a complex sequence).
|
|
88
89
|
|
|
89
90
|
```python
|
|
91
|
+
from enum import Enum
|
|
90
92
|
from typing import List, Tuple
|
|
91
93
|
from ddutils.annotation_helpers import is_complex_sequence
|
|
92
94
|
|
|
93
95
|
|
|
96
|
+
class Kind(str, Enum):
|
|
97
|
+
A = 'a'
|
|
98
|
+
|
|
99
|
+
|
|
94
100
|
is_complex_sequence(List[int]) # True
|
|
95
101
|
is_complex_sequence(Tuple[str, ...]) # True
|
|
96
102
|
is_complex_sequence(str) # False
|
|
103
|
+
is_complex_sequence(Kind) # False
|
|
97
104
|
is_complex_sequence(dict) # False
|
|
98
105
|
```
|
|
99
106
|
|
|
@@ -138,6 +145,21 @@ get_annotation_without_optional(Optional[int]) # <class 'int'>
|
|
|
138
145
|
get_annotation_without_optional(Optional[List[str]]) # typing.List[str]
|
|
139
146
|
```
|
|
140
147
|
|
|
148
|
+
### get_annotation_name
|
|
149
|
+
|
|
150
|
+
Returns a readable name of an annotation for error messages: a class, a union or a tuple of classes as
|
|
151
|
+
`isinstance` takes it.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from ddutils.annotation_helpers import get_annotation_name
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
get_annotation_name(int) # 'int'
|
|
158
|
+
get_annotation_name((int, float)) # 'int | float'
|
|
159
|
+
get_annotation_name(int | None) # 'int | None'
|
|
160
|
+
get_annotation_name(list[int]) # 'list[int]'
|
|
161
|
+
```
|
|
162
|
+
|
|
141
163
|
## Class Helpers
|
|
142
164
|
|
|
143
165
|
Helper functions for inspecting and extending class-level behavior
|
|
@@ -216,6 +238,36 @@ get_generic_base_argument(UserRepository, Repository) # <class 'User'>
|
|
|
216
238
|
get_generic_base_argument(Repository, Repository) # None
|
|
217
239
|
```
|
|
218
240
|
|
|
241
|
+
### require_class_attribute
|
|
242
|
+
|
|
243
|
+
Returns a class attribute that subclasses are expected to declare, for `__init_subclass__` checks. A missing,
|
|
244
|
+
`None` or empty value raises `AttributeError`, a value of the wrong type raises `TypeError`; both name the
|
|
245
|
+
attribute and the class, so a misconfigured subclass fails at definition rather than on first use.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from typing import ClassVar
|
|
249
|
+
|
|
250
|
+
from ddutils.class_helpers import require_class_attribute
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class BaseConsumer:
|
|
254
|
+
topic: ClassVar[str]
|
|
255
|
+
batch_size: ClassVar[int] = 100
|
|
256
|
+
|
|
257
|
+
def __init_subclass__(cls, **kwargs):
|
|
258
|
+
super().__init_subclass__(**kwargs)
|
|
259
|
+
require_class_attribute(cls, 'topic', str)
|
|
260
|
+
require_class_attribute(cls, 'batch_size', int)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class EventConsumer(BaseConsumer):
|
|
264
|
+
topic = 'events'
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class BrokenConsumer(BaseConsumer): # AttributeError: `topic` class attribute of BrokenConsumer must be set
|
|
268
|
+
pass
|
|
269
|
+
```
|
|
270
|
+
|
|
219
271
|
## Function Helpers
|
|
220
272
|
|
|
221
273
|
Tools for creating and modifying Python functions at runtime.
|
|
@@ -285,6 +337,30 @@ delta = timedelta(seconds=5, milliseconds=500)
|
|
|
285
337
|
convert_timedelta_to_milliseconds(delta) # 5500
|
|
286
338
|
```
|
|
287
339
|
|
|
340
|
+
### convert_to_builtin
|
|
341
|
+
|
|
342
|
+
Casts a value to its builtin base type (see [get_builtin_base_type](#get_builtin_base_type)):
|
|
343
|
+
a `str` subclass becomes a plain `str`, an `Enum` member becomes its `.value` cast the same way.
|
|
344
|
+
Raises `TypeError` for a value without a builtin base.
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from enum import Enum
|
|
348
|
+
from ddutils.convertors import convert_to_builtin
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class Kind(str, Enum):
|
|
352
|
+
A = 'a'
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class UserId(int):
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
convert_to_builtin(Kind.A) # 'a' (not 'Kind.A', which `str(Kind.A)` would give)
|
|
360
|
+
convert_to_builtin(UserId(2)) # 2
|
|
361
|
+
convert_to_builtin(object()) # TypeError
|
|
362
|
+
```
|
|
363
|
+
|
|
288
364
|
## Datetime Helpers
|
|
289
365
|
|
|
290
366
|
Utilities for working with dates and times.
|
|
@@ -446,6 +522,50 @@ AST-based introspection for extracting exceptions from function source code.
|
|
|
446
522
|
Parses a function’s source code to find all `raise` statements and identify the exception types they use.
|
|
447
523
|
Returns an iterator of `ExceptionInfo` objects describing each raised exception.
|
|
448
524
|
|
|
525
|
+
## Value Helpers
|
|
526
|
+
|
|
527
|
+
Runtime counterparts of the annotation helpers: they inspect values, not type hints.
|
|
528
|
+
|
|
529
|
+
### get_builtin_base_type
|
|
530
|
+
|
|
531
|
+
Returns the builtin type a value should be treated as, looking past subclasses: `bool`, `int`, `float`, `str` or `bytes`
|
|
532
|
+
(`bool` is checked before `int`). An `Enum` member is looked at through its `.value`.
|
|
533
|
+
Raises `TypeError` for any other value. Useful for APIs that dispatch on the exact type (serializers, query builders).
|
|
534
|
+
|
|
535
|
+
```python
|
|
536
|
+
from enum import Enum
|
|
537
|
+
from ddutils.value_helpers import get_builtin_base_type
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class Kind(str, Enum):
|
|
541
|
+
A = 'a'
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
class UserId(int):
|
|
545
|
+
pass
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
get_builtin_base_type(UserId(2)) # <class 'int'>
|
|
549
|
+
get_builtin_base_type(True) # <class 'bool'>
|
|
550
|
+
get_builtin_base_type(Kind.A) # <class 'str'>
|
|
551
|
+
get_builtin_base_type(object()) # TypeError
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### is_complex_sequence_value
|
|
555
|
+
|
|
556
|
+
Checks whether a value is a sequence other than `str`, `bytes` or `bytearray` (subclasses included).
|
|
557
|
+
Sets and mappings are not sequences. The runtime counterpart of [is_complex_sequence](#is_complex_sequence).
|
|
558
|
+
|
|
559
|
+
```python
|
|
560
|
+
from ddutils.value_helpers import is_complex_sequence_value
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
is_complex_sequence_value([1, 2]) # True
|
|
564
|
+
is_complex_sequence_value((1, 2)) # True
|
|
565
|
+
is_complex_sequence_value('abc') # False
|
|
566
|
+
is_complex_sequence_value({1, 2}) # False
|
|
567
|
+
```
|
|
568
|
+
|
|
449
569
|
## Sequence Helpers
|
|
450
570
|
|
|
451
571
|
Safe operations on sequence types.
|
|
@@ -66,16 +66,23 @@ is_subclass(List[str], dict) # False
|
|
|
66
66
|
|
|
67
67
|
### is_complex_sequence
|
|
68
68
|
|
|
69
|
-
Checks whether a type annotation represents a sequence type, excluding `str`, `bytes`, and `bytearray
|
|
69
|
+
Checks whether a type annotation represents a sequence type, excluding `str`, `bytes`, and `bytearray`
|
|
70
|
+
and their subclasses (a `str`-based `Enum` is not a complex sequence).
|
|
70
71
|
|
|
71
72
|
```python
|
|
73
|
+
from enum import Enum
|
|
72
74
|
from typing import List, Tuple
|
|
73
75
|
from ddutils.annotation_helpers import is_complex_sequence
|
|
74
76
|
|
|
75
77
|
|
|
78
|
+
class Kind(str, Enum):
|
|
79
|
+
A = 'a'
|
|
80
|
+
|
|
81
|
+
|
|
76
82
|
is_complex_sequence(List[int]) # True
|
|
77
83
|
is_complex_sequence(Tuple[str, ...]) # True
|
|
78
84
|
is_complex_sequence(str) # False
|
|
85
|
+
is_complex_sequence(Kind) # False
|
|
79
86
|
is_complex_sequence(dict) # False
|
|
80
87
|
```
|
|
81
88
|
|
|
@@ -120,6 +127,21 @@ get_annotation_without_optional(Optional[int]) # <class 'int'>
|
|
|
120
127
|
get_annotation_without_optional(Optional[List[str]]) # typing.List[str]
|
|
121
128
|
```
|
|
122
129
|
|
|
130
|
+
### get_annotation_name
|
|
131
|
+
|
|
132
|
+
Returns a readable name of an annotation for error messages: a class, a union or a tuple of classes as
|
|
133
|
+
`isinstance` takes it.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from ddutils.annotation_helpers import get_annotation_name
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
get_annotation_name(int) # 'int'
|
|
140
|
+
get_annotation_name((int, float)) # 'int | float'
|
|
141
|
+
get_annotation_name(int | None) # 'int | None'
|
|
142
|
+
get_annotation_name(list[int]) # 'list[int]'
|
|
143
|
+
```
|
|
144
|
+
|
|
123
145
|
## Class Helpers
|
|
124
146
|
|
|
125
147
|
Helper functions for inspecting and extending class-level behavior
|
|
@@ -198,6 +220,36 @@ get_generic_base_argument(UserRepository, Repository) # <class 'User'>
|
|
|
198
220
|
get_generic_base_argument(Repository, Repository) # None
|
|
199
221
|
```
|
|
200
222
|
|
|
223
|
+
### require_class_attribute
|
|
224
|
+
|
|
225
|
+
Returns a class attribute that subclasses are expected to declare, for `__init_subclass__` checks. A missing,
|
|
226
|
+
`None` or empty value raises `AttributeError`, a value of the wrong type raises `TypeError`; both name the
|
|
227
|
+
attribute and the class, so a misconfigured subclass fails at definition rather than on first use.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from typing import ClassVar
|
|
231
|
+
|
|
232
|
+
from ddutils.class_helpers import require_class_attribute
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class BaseConsumer:
|
|
236
|
+
topic: ClassVar[str]
|
|
237
|
+
batch_size: ClassVar[int] = 100
|
|
238
|
+
|
|
239
|
+
def __init_subclass__(cls, **kwargs):
|
|
240
|
+
super().__init_subclass__(**kwargs)
|
|
241
|
+
require_class_attribute(cls, 'topic', str)
|
|
242
|
+
require_class_attribute(cls, 'batch_size', int)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class EventConsumer(BaseConsumer):
|
|
246
|
+
topic = 'events'
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class BrokenConsumer(BaseConsumer): # AttributeError: `topic` class attribute of BrokenConsumer must be set
|
|
250
|
+
pass
|
|
251
|
+
```
|
|
252
|
+
|
|
201
253
|
## Function Helpers
|
|
202
254
|
|
|
203
255
|
Tools for creating and modifying Python functions at runtime.
|
|
@@ -267,6 +319,30 @@ delta = timedelta(seconds=5, milliseconds=500)
|
|
|
267
319
|
convert_timedelta_to_milliseconds(delta) # 5500
|
|
268
320
|
```
|
|
269
321
|
|
|
322
|
+
### convert_to_builtin
|
|
323
|
+
|
|
324
|
+
Casts a value to its builtin base type (see [get_builtin_base_type](#get_builtin_base_type)):
|
|
325
|
+
a `str` subclass becomes a plain `str`, an `Enum` member becomes its `.value` cast the same way.
|
|
326
|
+
Raises `TypeError` for a value without a builtin base.
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
from enum import Enum
|
|
330
|
+
from ddutils.convertors import convert_to_builtin
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class Kind(str, Enum):
|
|
334
|
+
A = 'a'
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class UserId(int):
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
convert_to_builtin(Kind.A) # 'a' (not 'Kind.A', which `str(Kind.A)` would give)
|
|
342
|
+
convert_to_builtin(UserId(2)) # 2
|
|
343
|
+
convert_to_builtin(object()) # TypeError
|
|
344
|
+
```
|
|
345
|
+
|
|
270
346
|
## Datetime Helpers
|
|
271
347
|
|
|
272
348
|
Utilities for working with dates and times.
|
|
@@ -428,6 +504,50 @@ AST-based introspection for extracting exceptions from function source code.
|
|
|
428
504
|
Parses a function’s source code to find all `raise` statements and identify the exception types they use.
|
|
429
505
|
Returns an iterator of `ExceptionInfo` objects describing each raised exception.
|
|
430
506
|
|
|
507
|
+
## Value Helpers
|
|
508
|
+
|
|
509
|
+
Runtime counterparts of the annotation helpers: they inspect values, not type hints.
|
|
510
|
+
|
|
511
|
+
### get_builtin_base_type
|
|
512
|
+
|
|
513
|
+
Returns the builtin type a value should be treated as, looking past subclasses: `bool`, `int`, `float`, `str` or `bytes`
|
|
514
|
+
(`bool` is checked before `int`). An `Enum` member is looked at through its `.value`.
|
|
515
|
+
Raises `TypeError` for any other value. Useful for APIs that dispatch on the exact type (serializers, query builders).
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
from enum import Enum
|
|
519
|
+
from ddutils.value_helpers import get_builtin_base_type
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class Kind(str, Enum):
|
|
523
|
+
A = 'a'
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class UserId(int):
|
|
527
|
+
pass
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
get_builtin_base_type(UserId(2)) # <class 'int'>
|
|
531
|
+
get_builtin_base_type(True) # <class 'bool'>
|
|
532
|
+
get_builtin_base_type(Kind.A) # <class 'str'>
|
|
533
|
+
get_builtin_base_type(object()) # TypeError
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### is_complex_sequence_value
|
|
537
|
+
|
|
538
|
+
Checks whether a value is a sequence other than `str`, `bytes` or `bytearray` (subclasses included).
|
|
539
|
+
Sets and mappings are not sequences. The runtime counterpart of [is_complex_sequence](#is_complex_sequence).
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
from ddutils.value_helpers import is_complex_sequence_value
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
is_complex_sequence_value([1, 2]) # True
|
|
546
|
+
is_complex_sequence_value((1, 2)) # True
|
|
547
|
+
is_complex_sequence_value('abc') # False
|
|
548
|
+
is_complex_sequence_value({1, 2}) # False
|
|
549
|
+
```
|
|
550
|
+
|
|
431
551
|
## Sequence Helpers
|
|
432
552
|
|
|
433
553
|
Safe operations on sequence types.
|
|
@@ -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
|
|
|
@@ -34,7 +34,7 @@ def is_subclass(annotation: Any, *base_types: Any) -> bool:
|
|
|
34
34
|
|
|
35
35
|
def is_complex_sequence(annotation: Any) -> bool:
|
|
36
36
|
annotation_origin = get_annotation_origin(annotation)
|
|
37
|
-
return
|
|
37
|
+
return not is_subclass(annotation_origin, *NON_COMPLEX_SEQUENCE_TYPES) and is_subclass(annotation_origin, Sequence)
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def get_complex_sequence_element_annotation(annotation: Any) -> Any:
|
|
@@ -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.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from datetime import timedelta
|
|
2
|
-
from typing import Protocol
|
|
2
|
+
from typing import Any, Protocol
|
|
3
|
+
|
|
4
|
+
from ddutils.value_helpers import get_builtin_base_type, unwrap_enum
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
class ClassInstanceWithDict(Protocol):
|
|
@@ -21,3 +23,12 @@ def convert_to_repr(obj: ClassInstanceWithDict) -> str:
|
|
|
21
23
|
|
|
22
24
|
def convert_timedelta_to_milliseconds(delta: timedelta) -> int:
|
|
23
25
|
return int(delta.total_seconds() * 1000)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def convert_to_builtin(value: Any) -> bool | int | float | str | bytes:
|
|
29
|
+
"""
|
|
30
|
+
Cast a value to its builtin base type (see `get_builtin_base_type`): a `str` subclass becomes a plain `str`,
|
|
31
|
+
an `Enum` member becomes its `.value` cast the same way. Raises `TypeError` for a value without a builtin base.
|
|
32
|
+
"""
|
|
33
|
+
value = unwrap_enum(value)
|
|
34
|
+
return get_builtin_base_type(value)(value)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ddutils.annotation_helpers import NON_COMPLEX_SEQUENCE_TYPES
|
|
6
|
+
|
|
7
|
+
# `bool` goes before `int` because `bool` is a subclass of `int`
|
|
8
|
+
BUILTIN_BASE_TYPES = (bool, int, float, str, bytes)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def unwrap_enum(value: Any) -> Any:
|
|
12
|
+
return value.value if isinstance(value, Enum) else value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_builtin_base_type(value: Any) -> type:
|
|
16
|
+
"""
|
|
17
|
+
Return the builtin type a value should be treated as, looking past subclasses: `bool`, `int`, `float`, `str`
|
|
18
|
+
or `bytes`. An `Enum` member is looked at through its `.value`. Raises `TypeError` for any other value.
|
|
19
|
+
"""
|
|
20
|
+
value = unwrap_enum(value)
|
|
21
|
+
for base_type in BUILTIN_BASE_TYPES:
|
|
22
|
+
if isinstance(value, base_type):
|
|
23
|
+
return base_type
|
|
24
|
+
|
|
25
|
+
raise TypeError(f'Value of type `{type(value).__name__}` has no builtin base type')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_complex_sequence_value(value: Any) -> bool:
|
|
29
|
+
"""Return whether a value is a sequence other than `str`, `bytes` or `bytearray`; subclasses are handled."""
|
|
30
|
+
return isinstance(value, Sequence) and not isinstance(value, NON_COMPLEX_SEQUENCE_TYPES)
|
|
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
|