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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddutils
3
- Version: 0.2.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.
@@ -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.3"
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.3"
5
5
  authors = [{ name = "davyddd" }]
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
File without changes
File without changes
File without changes
File without changes