ducktools-classbuilder 0.5.1__py3-none-any.whl → 0.6.1__py3-none-any.whl

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.

Potentially problematic release.


This version of ducktools-classbuilder might be problematic. Click here for more details.

@@ -1,43 +1,73 @@
1
+ import types
1
2
  import typing
3
+
2
4
  from collections.abc import Callable
5
+ from types import MappingProxyType
6
+ from typing_extensions import dataclass_transform
3
7
 
4
- _py_type = type # Alias for type where it is used as a name
8
+ _py_type = type | str # Alias for type hint values
9
+ _CopiableMappings = dict[str, typing.Any] | MappingProxyType[str, typing.Any]
5
10
 
6
11
  __version__: str
7
12
  INTERNALS_DICT: str
13
+ META_GATHERER_NAME: str
8
14
 
9
15
  def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...
10
16
 
11
- def get_flags(cls:type) -> dict[str, bool]: ...
17
+ def get_flags(cls: type) -> dict[str, bool]: ...
18
+
19
+ def get_methods(cls: type) -> types.MappingProxyType[str, MethodMaker]: ...
12
20
 
13
21
  def _get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
14
22
 
15
23
  class _NothingType:
16
- ...
24
+ def __repr__(self) -> str: ...
17
25
  NOTHING: _NothingType
18
26
 
27
+ # noinspection PyPep8Naming
28
+ class _KW_ONLY_TYPE:
29
+ def __repr__(self) -> str: ...
30
+
31
+ KW_ONLY: _KW_ONLY_TYPE
19
32
  # Stub Only
20
- _codegen_type = Callable[[type], tuple[str, dict[str, typing.Any]]]
33
+ @typing.type_check_only
34
+ class _CodegenType(typing.Protocol):
35
+ def __call__(self, cls: type, funcname: str = ...) -> GeneratedCode: ...
36
+
37
+
38
+ class GeneratedCode:
39
+ __slots__: tuple[str, str]
40
+ source_code: str
41
+ globs: dict[str, typing.Any]
42
+
43
+ def __init__(self, source_code: str, globs: dict[str, typing.Any]) -> None: ...
44
+ def __repr__(self) -> str: ...
45
+
21
46
 
22
47
  class MethodMaker:
23
48
  funcname: str
24
- code_generator: _codegen_type
25
- def __init__(self, funcname: str, code_generator: _codegen_type) -> None: ...
49
+ code_generator: _CodegenType
50
+ def __init__(self, funcname: str, code_generator: _CodegenType) -> None: ...
26
51
  def __repr__(self) -> str: ...
27
- def __get__(self, instance, cls) -> Callable: ...
52
+ def __get__(self, instance, cls=None) -> Callable: ...
28
53
 
29
54
  def get_init_generator(
30
55
  null: _NothingType = NOTHING,
31
56
  extra_code: None | list[str] = None
32
- ) -> Callable[[type], tuple[str, dict[str, typing.Any]]]: ...
57
+ ) -> _CodegenType: ...
33
58
 
34
- def init_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
35
- def repr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
36
- def eq_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
59
+ def init_generator(cls: type, funcname: str="__init__") -> GeneratedCode: ...
37
60
 
38
- def frozen_setattr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
61
+ def get_repr_generator(
62
+ recursion_safe: bool = False,
63
+ eval_safe: bool = False
64
+ ) -> _CodegenType: ...
65
+ def repr_generator(cls: type, funcname: str = "__repr__") -> GeneratedCode: ...
66
+ def eq_generator(cls: type, funcname: str = "__eq__") -> GeneratedCode: ...
39
67
 
40
- def frozen_delattr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
68
+ def frozen_setattr_generator(cls: type, funcname: str = "__setattr__") -> GeneratedCode: ...
69
+
70
+ def frozen_delattr_generator(cls: type, funcname: str = "__delattr__") -> GeneratedCode: ...
41
71
 
42
72
  init_maker: MethodMaker
43
73
  repr_maker: MethodMaker
@@ -69,12 +99,32 @@ def builder(
69
99
  ) -> Callable[[type[_T]], type[_T]]: ...
70
100
 
71
101
 
72
- class Field:
102
+ class SlotFields(dict):
103
+ ...
104
+
105
+
106
+ class SlotMakerMeta(type):
107
+ def __new__(
108
+ cls: type[_T],
109
+ name: str,
110
+ bases: tuple[type, ...],
111
+ ns: dict[str, typing.Any],
112
+ slots: bool = True,
113
+ **kwargs: typing.Any,
114
+ ) -> _T: ...
115
+
116
+
117
+ class Field(metaclass=SlotMakerMeta):
73
118
  default: _NothingType | typing.Any
74
119
  default_factory: _NothingType | typing.Any
75
120
  type: _NothingType | _py_type
76
121
  doc: None | str
122
+ init: bool
123
+ repr: bool
124
+ compare: bool
125
+ kw_only: bool
77
126
 
127
+ __slots__: dict[str, str]
78
128
  __classbuilder_internals__: dict
79
129
 
80
130
  def __init__(
@@ -84,7 +134,13 @@ class Field:
84
134
  default_factory: _NothingType | typing.Any = NOTHING,
85
135
  type: _NothingType | _py_type = NOTHING,
86
136
  doc: None | str = None,
137
+ init: bool = True,
138
+ repr: bool = True,
139
+ compare: bool = True,
140
+ kw_only: bool = False,
87
141
  ) -> None: ...
142
+
143
+ def __init_subclass__(cls, frozen: bool = False): ...
88
144
  def __repr__(self) -> str: ...
89
145
  def __eq__(self, other: Field | object) -> bool: ...
90
146
  def validate_field(self) -> None: ...
@@ -92,43 +148,64 @@ class Field:
92
148
  def from_field(cls, fld: Field, /, **kwargs: typing.Any) -> Field: ...
93
149
 
94
150
 
95
- class GatheredFields:
96
- __slots__ = ("fields", "modifications")
97
-
98
- fields: dict[str, Field]
99
- modifications: dict[str, typing.Any]
151
+ # type[Field] doesn't work due to metaclass
152
+ # This is not really precise enough because isinstance is used
153
+ _ReturnsField = Callable[..., Field]
154
+ _FieldType = typing.TypeVar("_FieldType", bound=Field)
100
155
 
101
- __classbuilder_internals__: dict
102
156
 
103
- def __init__(
104
- self,
105
- fields: dict[str, Field],
106
- modifications: dict[str, typing.Any]
107
- ) -> None: ...
157
+ @typing.overload
158
+ def make_slot_gatherer(
159
+ field_type: type[_FieldType]
160
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
108
161
 
109
- def __repr__(self) -> str: ...
110
- def __eq__(self, other) -> bool: ...
111
- def __call__(self, cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
162
+ @typing.overload
163
+ def make_slot_gatherer(
164
+ field_type: _ReturnsField = Field
165
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
112
166
 
167
+ @typing.overload
168
+ def make_annotation_gatherer(
169
+ field_type: type[_FieldType],
170
+ leave_default_values: bool = True,
171
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
113
172
 
114
- class SlotFields(dict):
115
- ...
173
+ @typing.overload
174
+ def make_annotation_gatherer(
175
+ field_type: _ReturnsField = Field,
176
+ leave_default_values: bool = True,
177
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
116
178
 
117
- def make_slot_gatherer(
118
- field_type: type[Field] = Field
119
- ) -> Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
179
+ @typing.overload
180
+ def make_field_gatherer(
181
+ field_type: type[_FieldType],
182
+ leave_default_values: bool = True,
183
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
120
184
 
121
- def slot_gatherer(cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]:
122
- ...
185
+ @typing.overload
186
+ def make_field_gatherer(
187
+ field_type: _ReturnsField = Field,
188
+ leave_default_values: bool = True,
189
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
123
190
 
124
- def is_classvar(hint: object) -> bool: ...
191
+ @typing.overload
192
+ def make_unified_gatherer(
193
+ field_type: type[_FieldType],
194
+ leave_default_values: bool = True,
195
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
125
196
 
126
- def make_annotation_gatherer(
127
- field_type: type[Field] = Field,
197
+ @typing.overload
198
+ def make_unified_gatherer(
199
+ field_type: _ReturnsField = Field,
128
200
  leave_default_values: bool = True,
129
- ) -> Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
201
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
202
+
203
+
204
+ def slot_gatherer(cls_or_ns: type | _CopiableMappings) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
205
+ def annotation_gatherer(cls_or_ns: type | _CopiableMappings) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
206
+
207
+ def unified_gatherer(cls_or_ns: type | _CopiableMappings) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
130
208
 
131
- def annotation_gatherer(cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
132
209
 
133
210
  def check_argument_order(cls: type) -> None: ...
134
211
 
@@ -150,24 +227,35 @@ def slotclass(
150
227
  syntax_check: bool = True
151
228
  ) -> Callable[[type[_T]], type[_T]]: ...
152
229
 
153
- @typing.overload
154
- def annotationclass(
155
- cls: type[_T],
156
- /,
157
- *,
158
- methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
159
- ) -> type[_T]: ...
160
230
 
161
- @typing.overload
162
- def annotationclass(
163
- cls: None = None,
164
- /,
165
- *,
231
+ _gatherer_type = Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]
232
+
233
+
234
+ @dataclass_transform(field_specifiers=(Field,))
235
+ class AnnotationClass(metaclass=SlotMakerMeta):
236
+ __slots__: dict
237
+
238
+ def __init_subclass__(
239
+ cls,
166
240
  methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
167
- ) -> Callable[[type[_T]], type[_T]]: ...
241
+ gatherer: _gatherer_type = make_unified_gatherer(leave_default_values=True),
242
+ **kwargs,
243
+ ) -> None: ...
168
244
 
169
- @typing.overload
170
- def fieldclass(cls: type[_T], /, *, frozen: bool = False) -> type[_T]: ...
245
+ class GatheredFields:
246
+ __slots__: dict[str, None]
171
247
 
172
- @typing.overload
173
- def fieldclass(cls: None = None, /, *, frozen: bool = False) -> Callable[[type[_T]], type[_T]]: ...
248
+ fields: dict[str, Field]
249
+ modifications: dict[str, typing.Any]
250
+
251
+ __classbuilder_internals__: dict
252
+
253
+ def __init__(
254
+ self,
255
+ fields: dict[str, Field],
256
+ modifications: dict[str, typing.Any]
257
+ ) -> None: ...
258
+
259
+ def __repr__(self) -> str: ...
260
+ def __eq__(self, other) -> bool: ...
261
+ def __call__(self, cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
@@ -0,0 +1,173 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2024 David C Ellis
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ import sys
24
+ import builtins
25
+
26
+
27
+ class _StringGlobs(dict):
28
+ """
29
+ Based on the fake globals dictionary used for annotations
30
+ from 3.14. This allows us to evaluate containers which
31
+ include forward references.
32
+
33
+ It's just a dictionary that returns the key if the key
34
+ is not found.
35
+ """
36
+ def __missing__(self, key):
37
+ return key
38
+
39
+ def __repr__(self):
40
+ cls_name = self.__class__.__name__
41
+ dict_repr = super().__repr__()
42
+ return f"{cls_name}({dict_repr})"
43
+
44
+
45
+ def eval_hint(hint, context=None, *, recursion_limit=2):
46
+ """
47
+ Attempt to evaluate a string type hint in the given
48
+ context.
49
+
50
+ If this raises an exception, return the last string.
51
+
52
+ If the recursion limit is hit or a previous value returns
53
+ on evaluation, return the original hint string.
54
+
55
+ Example::
56
+ import builtins
57
+ from typing import ClassVar
58
+
59
+ from ducktools.classbuilder.annotations import eval_hint
60
+
61
+ foo = "foo"
62
+
63
+ context = {**vars(builtins), **globals(), **locals()}
64
+ eval_hint("foo", context) # returns 'foo'
65
+
66
+ eval_hint("ClassVar[str]", context) # returns typing.ClassVar[str]
67
+ eval_hint("ClassVar[forwardref]", context) # returns typing.ClassVar[ForwardRef('forwardref')]
68
+
69
+ :param hint: The existing type hint
70
+ :param context: merged context
71
+ :param recursion_limit: maximum number of evaluation loops before
72
+ returning the original string.
73
+ :return: evaluated hint, or string if it could not evaluate
74
+ """
75
+ if context is not None:
76
+ context = _StringGlobs(context)
77
+
78
+ original_hint = hint
79
+
80
+ # Using a set would require the hint always be hashable
81
+ # This is only going to be 2 items at most usually
82
+ seen = []
83
+ i = 0
84
+ while isinstance(hint, str):
85
+ seen.append(hint)
86
+
87
+ # noinspection PyBroadException
88
+ try:
89
+ hint = eval(hint, context)
90
+ except Exception:
91
+ break
92
+
93
+ if hint in seen or i >= recursion_limit:
94
+ hint = original_hint
95
+ break
96
+
97
+ i += 1
98
+
99
+ return hint
100
+
101
+
102
+ def get_ns_annotations(ns, eval_str=True):
103
+ """
104
+ Given a class namespace, attempt to retrieve the
105
+ annotations dictionary and evaluate strings.
106
+
107
+ Note: This only evaluates in the context of module level globals
108
+ and values in the class namespace. Non-local variables will not
109
+ be evaluated.
110
+
111
+ :param ns: Class namespace (eg cls.__dict__)
112
+ :param eval_str: Attempt to evaluate string annotations (default to True)
113
+ :return: dictionary of evaluated annotations
114
+ """
115
+ raw_annotations = ns.get("__annotations__", {})
116
+
117
+ if not eval_str:
118
+ return raw_annotations.copy()
119
+
120
+ try:
121
+ obj_modulename = ns["__module__"]
122
+ except KeyError:
123
+ obj_module = None
124
+ else:
125
+ obj_module = sys.modules.get(obj_modulename, None)
126
+
127
+ if obj_module:
128
+ obj_globals = vars(obj_module)
129
+ else:
130
+ obj_globals = {}
131
+
132
+ # Type parameters should be usable in hints without breaking
133
+ # This is for Python 3.12+
134
+ type_params = {
135
+ repr(param): param
136
+ for param in ns.get("__type_params__", ())
137
+ }
138
+
139
+ context = {**vars(builtins), **obj_globals, **type_params, **ns}
140
+
141
+ return {
142
+ k: eval_hint(v, context)
143
+ for k, v in raw_annotations.items()
144
+ }
145
+
146
+
147
+ def is_classvar(hint):
148
+ _typing = sys.modules.get("typing")
149
+ if _typing:
150
+ # Annotated is a nightmare I'm never waking up from
151
+ # 3.8 and 3.9 need Annotated from typing_extensions
152
+ # 3.8 also needs get_origin from typing_extensions
153
+ if sys.version_info < (3, 10):
154
+ _typing_extensions = sys.modules.get("typing_extensions")
155
+ if _typing_extensions:
156
+ _Annotated = _typing_extensions.Annotated
157
+ _get_origin = _typing_extensions.get_origin
158
+ else:
159
+ _Annotated, _get_origin = None, None
160
+ else:
161
+ _Annotated = _typing.Annotated
162
+ _get_origin = _typing.get_origin
163
+
164
+ if _Annotated and _get_origin(hint) is _Annotated:
165
+ hint = getattr(hint, "__origin__", None)
166
+
167
+ if (
168
+ hint is _typing.ClassVar
169
+ or getattr(hint, "__origin__", None) is _typing.ClassVar
170
+ ):
171
+ return True
172
+ return False
173
+
@@ -0,0 +1,26 @@
1
+ import typing
2
+ import types
3
+
4
+ _T = typing.TypeVar("_T")
5
+ _CopiableMappings = dict[str, typing.Any] | types.MappingProxyType[str, typing.Any]
6
+
7
+ class _StringGlobs(dict):
8
+ def __missing__(self, key: _T) -> _T: ...
9
+
10
+
11
+ def eval_hint(
12
+ hint: type | str,
13
+ context: None | dict[str, typing.Any] = None,
14
+ *,
15
+ recursion_limit: int = 2
16
+ ) -> type | str: ...
17
+
18
+
19
+ def get_ns_annotations(
20
+ ns: _CopiableMappings,
21
+ eval_str: bool = True,
22
+ ) -> dict[str, typing.Any]: ...
23
+
24
+ def is_classvar(
25
+ hint: object,
26
+ ) -> bool: ...