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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddutils
3
- Version: 0.2.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 annotation_origin not in NON_COMPLEX_SEQUENCE_TYPES and is_subclass(annotation_origin, Sequence)
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)
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "ddutils"
3
3
  description = "Domain Driven Utils Library"
4
- version = "0.2.2"
4
+ version = "0.2.4"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
7
7
  readme = "README.md"
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "ddutils"
3
3
  description = "Domain Driven Utils Library"
4
- version = "0.2.2"
4
+ version = "0.2.4"
5
5
  authors = [{ name = "davyddd" }]
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
File without changes
File without changes
File without changes