ducktools-classbuilder 0.5.0__py3-none-any.whl → 0.6.0__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,70 @@
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
+ _codegen_type = Callable[[type], GeneratedCode]
34
+
35
+ class GeneratedCode:
36
+ __slots__: tuple[str, str]
37
+ source_code: str
38
+ globs: dict[str, typing.Any]
39
+
40
+ def __init__(self, source_code: str, globs: dict[str, typing.Any]) -> None: ...
41
+ def __repr__(self) -> str: ...
42
+
21
43
 
22
44
  class MethodMaker:
23
45
  funcname: str
24
46
  code_generator: _codegen_type
25
47
  def __init__(self, funcname: str, code_generator: _codegen_type) -> None: ...
26
48
  def __repr__(self) -> str: ...
27
- def __get__(self, instance, cls) -> Callable: ...
49
+ def __get__(self, instance, cls=None) -> Callable: ...
28
50
 
29
51
  def get_init_generator(
30
52
  null: _NothingType = NOTHING,
31
53
  extra_code: None | list[str] = None
32
- ) -> Callable[[type], tuple[str, dict[str, typing.Any]]]: ...
54
+ ) -> Callable[[type], GeneratedCode]: ...
55
+
56
+ def init_generator(cls: type) -> GeneratedCode: ...
33
57
 
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]]: ...
58
+ def get_repr_generator(
59
+ recursion_safe: bool = False,
60
+ eval_safe: bool = False
61
+ ) -> Callable[[type], GeneratedCode]: ...
62
+ def repr_generator(cls: type) -> GeneratedCode: ...
63
+ def eq_generator(cls: type) -> GeneratedCode: ...
37
64
 
38
- def frozen_setattr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
65
+ def frozen_setattr_generator(cls: type) -> GeneratedCode: ...
39
66
 
40
- def frozen_delattr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
67
+ def frozen_delattr_generator(cls: type) -> GeneratedCode: ...
41
68
 
42
69
  init_maker: MethodMaker
43
70
  repr_maker: MethodMaker
@@ -69,12 +96,32 @@ def builder(
69
96
  ) -> Callable[[type[_T]], type[_T]]: ...
70
97
 
71
98
 
72
- class Field:
99
+ class SlotFields(dict):
100
+ ...
101
+
102
+
103
+ class SlotMakerMeta(type):
104
+ def __new__(
105
+ cls: type[_T],
106
+ name: str,
107
+ bases: tuple[type, ...],
108
+ ns: dict[str, typing.Any],
109
+ slots: bool = True,
110
+ **kwargs: typing.Any,
111
+ ) -> _T: ...
112
+
113
+
114
+ class Field(metaclass=SlotMakerMeta):
73
115
  default: _NothingType | typing.Any
74
116
  default_factory: _NothingType | typing.Any
75
117
  type: _NothingType | _py_type
76
118
  doc: None | str
119
+ init: bool
120
+ repr: bool
121
+ compare: bool
122
+ kw_only: bool
77
123
 
124
+ __slots__: dict[str, str]
78
125
  __classbuilder_internals__: dict
79
126
 
80
127
  def __init__(
@@ -84,7 +131,13 @@ class Field:
84
131
  default_factory: _NothingType | typing.Any = NOTHING,
85
132
  type: _NothingType | _py_type = NOTHING,
86
133
  doc: None | str = None,
134
+ init: bool = True,
135
+ repr: bool = True,
136
+ compare: bool = True,
137
+ kw_only: bool = False,
87
138
  ) -> None: ...
139
+
140
+ def __init_subclass__(cls, frozen: bool = False): ...
88
141
  def __repr__(self) -> str: ...
89
142
  def __eq__(self, other: Field | object) -> bool: ...
90
143
  def validate_field(self) -> None: ...
@@ -92,24 +145,64 @@ class Field:
92
145
  def from_field(cls, fld: Field, /, **kwargs: typing.Any) -> Field: ...
93
146
 
94
147
 
95
- class SlotFields(dict):
96
- ...
148
+ # type[Field] doesn't work due to metaclass
149
+ # This is not really precise enough because isinstance is used
150
+ _ReturnsField = Callable[..., Field]
151
+ _FieldType = typing.TypeVar("_FieldType", bound=Field)
97
152
 
153
+
154
+ @typing.overload
98
155
  def make_slot_gatherer(
99
- field_type: type[Field] = Field
100
- ) -> Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
156
+ field_type: type[_FieldType]
157
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
101
158
 
102
- def slot_gatherer(cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]:
103
- ...
159
+ @typing.overload
160
+ def make_slot_gatherer(
161
+ field_type: _ReturnsField = Field
162
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
104
163
 
105
- def is_classvar(hint: object) -> bool: ...
164
+ @typing.overload
165
+ def make_annotation_gatherer(
166
+ field_type: type[_FieldType],
167
+ leave_default_values: bool = True,
168
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
106
169
 
170
+ @typing.overload
107
171
  def make_annotation_gatherer(
108
- field_type: type[Field] = Field,
172
+ field_type: _ReturnsField = Field,
173
+ leave_default_values: bool = True,
174
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
175
+
176
+ @typing.overload
177
+ def make_field_gatherer(
178
+ field_type: type[_FieldType],
109
179
  leave_default_values: bool = True,
110
- ) -> Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
180
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
181
+
182
+ @typing.overload
183
+ def make_field_gatherer(
184
+ field_type: _ReturnsField = Field,
185
+ leave_default_values: bool = True,
186
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
187
+
188
+ @typing.overload
189
+ def make_unified_gatherer(
190
+ field_type: type[_FieldType],
191
+ leave_default_values: bool = True,
192
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
193
+
194
+ @typing.overload
195
+ def make_unified_gatherer(
196
+ field_type: _ReturnsField = Field,
197
+ leave_default_values: bool = True,
198
+ ) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
199
+
200
+
201
+ def slot_gatherer(cls_or_ns: type | _CopiableMappings) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
202
+ def annotation_gatherer(cls_or_ns: type | _CopiableMappings) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
203
+
204
+ def unified_gatherer(cls_or_ns: type | _CopiableMappings) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
111
205
 
112
- def annotation_gatherer(cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
113
206
 
114
207
  def check_argument_order(cls: type) -> None: ...
115
208
 
@@ -131,24 +224,35 @@ def slotclass(
131
224
  syntax_check: bool = True
132
225
  ) -> Callable[[type[_T]], type[_T]]: ...
133
226
 
134
- @typing.overload
135
- def annotationclass(
136
- cls: type[_T],
137
- /,
138
- *,
139
- methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
140
- ) -> type[_T]: ...
141
227
 
142
- @typing.overload
143
- def annotationclass(
144
- cls: None = None,
145
- /,
146
- *,
228
+ _gatherer_type = Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]
229
+
230
+
231
+ @dataclass_transform(field_specifiers=(Field,))
232
+ class AnnotationClass(metaclass=SlotMakerMeta):
233
+ __slots__: dict
234
+
235
+ def __init_subclass__(
236
+ cls,
147
237
  methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
148
- ) -> Callable[[type[_T]], type[_T]]: ...
238
+ gatherer: _gatherer_type = make_unified_gatherer(leave_default_values=True),
239
+ **kwargs,
240
+ ) -> None: ...
149
241
 
150
- @typing.overload
151
- def fieldclass(cls: type[_T], /, *, frozen: bool = False) -> type[_T]: ...
242
+ class GatheredFields:
243
+ __slots__: dict[str, None]
152
244
 
153
- @typing.overload
154
- def fieldclass(cls: None = None, /, *, frozen: bool = False) -> Callable[[type[_T]], type[_T]]: ...
245
+ fields: dict[str, Field]
246
+ modifications: dict[str, typing.Any]
247
+
248
+ __classbuilder_internals__: dict
249
+
250
+ def __init__(
251
+ self,
252
+ fields: dict[str, Field],
253
+ modifications: dict[str, typing.Any]
254
+ ) -> None: ...
255
+
256
+ def __repr__(self) -> str: ...
257
+ def __eq__(self, other) -> bool: ...
258
+ 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:
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: ...