ducktools-classbuilder 0.12.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.
- ducktools/classbuilder/__init__.py +1252 -0
- ducktools/classbuilder/__init__.pyi +283 -0
- ducktools/classbuilder/_version.py +2 -0
- ducktools/classbuilder/annotations/__init__.py +63 -0
- ducktools/classbuilder/annotations/annotations_314.py +104 -0
- ducktools/classbuilder/annotations/annotations_pre_314.py +42 -0
- ducktools/classbuilder/annotations.pyi +21 -0
- ducktools/classbuilder/prefab.py +884 -0
- ducktools/classbuilder/prefab.pyi +251 -0
- ducktools/classbuilder/py.typed +1 -0
- ducktools_classbuilder-0.12.1.dist-info/METADATA +335 -0
- ducktools_classbuilder-0.12.1.dist-info/RECORD +15 -0
- ducktools_classbuilder-0.12.1.dist-info/WHEEL +5 -0
- ducktools_classbuilder-0.12.1.dist-info/licenses/LICENSE +21 -0
- ducktools_classbuilder-0.12.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,884 @@
|
|
|
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
|
+
"""
|
|
24
|
+
A 'prebuilt' implementation of class generation.
|
|
25
|
+
|
|
26
|
+
Includes pre and post init functions along with other methods.
|
|
27
|
+
"""
|
|
28
|
+
from . import (
|
|
29
|
+
INTERNALS_DICT, NOTHING, FIELD_NOTHING,
|
|
30
|
+
Field, MethodMaker, GatheredFields, GeneratedCode, SlotMakerMeta,
|
|
31
|
+
builder, get_flags, get_fields,
|
|
32
|
+
make_unified_gatherer,
|
|
33
|
+
eq_maker, frozen_setattr_maker, frozen_delattr_maker, replace_maker,
|
|
34
|
+
get_repr_generator,
|
|
35
|
+
build_completed,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .annotations import get_func_annotations
|
|
39
|
+
|
|
40
|
+
# These aren't used but are re-exported for ease of use
|
|
41
|
+
from . import SlotFields as SlotFields, KW_ONLY as KW_ONLY
|
|
42
|
+
|
|
43
|
+
PREFAB_FIELDS = "PREFAB_FIELDS"
|
|
44
|
+
PREFAB_INIT_FUNC = "__prefab_init__"
|
|
45
|
+
PRE_INIT_FUNC = "__prefab_pre_init__"
|
|
46
|
+
POST_INIT_FUNC = "__prefab_post_init__"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PrefabError(Exception):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_attributes(cls):
|
|
54
|
+
"""
|
|
55
|
+
Copy of get_fields, typed to return Attribute instead of Field.
|
|
56
|
+
This is used in the prefab methods.
|
|
57
|
+
|
|
58
|
+
:param cls: class built with _make_prefab
|
|
59
|
+
:return: dict[str, Attribute] of all gathered attributes
|
|
60
|
+
"""
|
|
61
|
+
return getattr(cls, INTERNALS_DICT)["fields"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Method Generators
|
|
65
|
+
def init_generator(cls, funcname="__init__"):
|
|
66
|
+
globs = {}
|
|
67
|
+
annotations = {}
|
|
68
|
+
# Get the internals dictionary and prepare attributes
|
|
69
|
+
attributes = get_attributes(cls)
|
|
70
|
+
flags = get_flags(cls)
|
|
71
|
+
|
|
72
|
+
kw_only = flags.get("kw_only", False)
|
|
73
|
+
|
|
74
|
+
# Handle pre/post init first - post_init can change types for __init__
|
|
75
|
+
# Get pre and post init arguments
|
|
76
|
+
pre_init_args = []
|
|
77
|
+
post_init_args = []
|
|
78
|
+
post_init_annotations = {}
|
|
79
|
+
|
|
80
|
+
for extra_funcname, func_arglist in [
|
|
81
|
+
(PRE_INIT_FUNC, pre_init_args),
|
|
82
|
+
(POST_INIT_FUNC, post_init_args),
|
|
83
|
+
]:
|
|
84
|
+
try:
|
|
85
|
+
func = getattr(cls, extra_funcname)
|
|
86
|
+
func_code = func.__code__
|
|
87
|
+
except AttributeError:
|
|
88
|
+
pass
|
|
89
|
+
else:
|
|
90
|
+
argcount = func_code.co_argcount + func_code.co_kwonlyargcount
|
|
91
|
+
|
|
92
|
+
# Identify if method is static, if so include first arg, otherwise skip
|
|
93
|
+
is_static = type(cls.__dict__.get(extra_funcname)) is staticmethod
|
|
94
|
+
|
|
95
|
+
arglist = (
|
|
96
|
+
func_code.co_varnames[:argcount]
|
|
97
|
+
if is_static
|
|
98
|
+
else func_code.co_varnames[1:argcount]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
func_arglist.extend(arglist)
|
|
102
|
+
|
|
103
|
+
if extra_funcname == POST_INIT_FUNC:
|
|
104
|
+
post_init_annotations |= get_func_annotations(func)
|
|
105
|
+
|
|
106
|
+
pos_arglist = []
|
|
107
|
+
kw_only_arglist = []
|
|
108
|
+
for name, attrib in attributes.items():
|
|
109
|
+
# post_init annotations can be used to broaden types.
|
|
110
|
+
if attrib.init:
|
|
111
|
+
if name in post_init_annotations:
|
|
112
|
+
annotations[name] = post_init_annotations[name]
|
|
113
|
+
elif attrib.type is not NOTHING:
|
|
114
|
+
annotations[name] = attrib.type
|
|
115
|
+
|
|
116
|
+
if attrib.default is not NOTHING:
|
|
117
|
+
if isinstance(attrib.default, (str, int, float, bool)):
|
|
118
|
+
# Just use the literal in these cases
|
|
119
|
+
arg = f"{name}={attrib.default!r}"
|
|
120
|
+
else:
|
|
121
|
+
# No guarantee repr will work for other objects
|
|
122
|
+
# so store the value in a variable and put it
|
|
123
|
+
# in the globals dict for eval
|
|
124
|
+
arg = f"{name}=_{name}_default"
|
|
125
|
+
globs[f"_{name}_default"] = attrib.default
|
|
126
|
+
elif attrib.default_factory is not NOTHING:
|
|
127
|
+
# Use NONE here and call the factory later
|
|
128
|
+
# This matches the behaviour of compiled
|
|
129
|
+
arg = f"{name}=None"
|
|
130
|
+
globs[f"_{name}_factory"] = attrib.default_factory
|
|
131
|
+
else:
|
|
132
|
+
arg = name
|
|
133
|
+
if attrib.kw_only or kw_only:
|
|
134
|
+
kw_only_arglist.append(arg)
|
|
135
|
+
else:
|
|
136
|
+
pos_arglist.append(arg)
|
|
137
|
+
# Not in init, but need to set defaults
|
|
138
|
+
else:
|
|
139
|
+
if attrib.default is not NOTHING:
|
|
140
|
+
globs[f"_{name}_default"] = attrib.default
|
|
141
|
+
elif attrib.default_factory is not NOTHING:
|
|
142
|
+
globs[f"_{name}_factory"] = attrib.default_factory
|
|
143
|
+
|
|
144
|
+
pos_args = ", ".join(pos_arglist)
|
|
145
|
+
kw_args = ", ".join(kw_only_arglist)
|
|
146
|
+
if pos_args and kw_args:
|
|
147
|
+
args = f"{pos_args}, *, {kw_args}"
|
|
148
|
+
elif kw_args:
|
|
149
|
+
args = f"*, {kw_args}"
|
|
150
|
+
else:
|
|
151
|
+
args = pos_args
|
|
152
|
+
|
|
153
|
+
assignments = []
|
|
154
|
+
processes = [] # post_init values still need default factories to be called.
|
|
155
|
+
for name, attrib in attributes.items():
|
|
156
|
+
if attrib.init:
|
|
157
|
+
if attrib.default_factory is not NOTHING:
|
|
158
|
+
value = f"{name} if {name} is not None else _{name}_factory()"
|
|
159
|
+
else:
|
|
160
|
+
value = name
|
|
161
|
+
else:
|
|
162
|
+
if attrib.default_factory is not NOTHING:
|
|
163
|
+
value = f"_{name}_factory()"
|
|
164
|
+
elif attrib.default is not NOTHING:
|
|
165
|
+
value = f"_{name}_default"
|
|
166
|
+
else:
|
|
167
|
+
value = None
|
|
168
|
+
|
|
169
|
+
if name in post_init_args:
|
|
170
|
+
if attrib.default_factory is not NOTHING:
|
|
171
|
+
processes.append((name, value))
|
|
172
|
+
elif value is not None:
|
|
173
|
+
assignments.append((name, value))
|
|
174
|
+
|
|
175
|
+
if hasattr(cls, PRE_INIT_FUNC):
|
|
176
|
+
pre_init_arg_call = ", ".join(f"{name}={name}" for name in pre_init_args)
|
|
177
|
+
pre_init_call = f" self.{PRE_INIT_FUNC}({pre_init_arg_call})\n"
|
|
178
|
+
else:
|
|
179
|
+
pre_init_call = ""
|
|
180
|
+
|
|
181
|
+
if assignments or processes:
|
|
182
|
+
body = ""
|
|
183
|
+
body += "\n".join(
|
|
184
|
+
f" self.{name} = {value}" for name, value in assignments
|
|
185
|
+
)
|
|
186
|
+
if assignments:
|
|
187
|
+
body += "\n"
|
|
188
|
+
body += "\n".join(f" {name} = {value}" for name, value in processes)
|
|
189
|
+
else:
|
|
190
|
+
body = " pass"
|
|
191
|
+
|
|
192
|
+
if hasattr(cls, POST_INIT_FUNC):
|
|
193
|
+
post_init_arg_call = ", ".join(f"{name}={name}" for name in post_init_args)
|
|
194
|
+
post_init_call = f" self.{POST_INIT_FUNC}({post_init_arg_call})\n"
|
|
195
|
+
else:
|
|
196
|
+
post_init_call = ""
|
|
197
|
+
|
|
198
|
+
code = (
|
|
199
|
+
f"def {funcname}(self, {args}):\n"
|
|
200
|
+
f"{pre_init_call}"
|
|
201
|
+
f"{body}\n"
|
|
202
|
+
f"{post_init_call}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if annotations:
|
|
206
|
+
annotations["return"] = None
|
|
207
|
+
else:
|
|
208
|
+
# If there are no annotations, return an unannotated init function
|
|
209
|
+
annotations = None
|
|
210
|
+
|
|
211
|
+
return GeneratedCode(code, globs, annotations)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def iter_generator(cls, funcname="__iter__"):
|
|
215
|
+
fields = get_attributes(cls)
|
|
216
|
+
|
|
217
|
+
valid_fields = (
|
|
218
|
+
name for name, attrib in fields.items()
|
|
219
|
+
if attrib.iter
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
values = "\n".join(f" yield self.{name}" for name in valid_fields)
|
|
223
|
+
|
|
224
|
+
# if values is an empty string
|
|
225
|
+
if not values:
|
|
226
|
+
values = " yield from ()"
|
|
227
|
+
|
|
228
|
+
code = f"def {funcname}(self):\n{values}"
|
|
229
|
+
globs = {}
|
|
230
|
+
return GeneratedCode(code, globs)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def as_dict_generator(cls, funcname="as_dict"):
|
|
234
|
+
fields = get_attributes(cls)
|
|
235
|
+
|
|
236
|
+
vals = ", ".join(
|
|
237
|
+
f"'{name}': self.{name}"
|
|
238
|
+
for name, attrib in fields.items()
|
|
239
|
+
if attrib.serialize
|
|
240
|
+
)
|
|
241
|
+
out_dict = f"{{{vals}}}"
|
|
242
|
+
code = f"def {funcname}(self): return {out_dict}\n"
|
|
243
|
+
|
|
244
|
+
globs = {}
|
|
245
|
+
return GeneratedCode(code, globs)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def hash_generator(cls, funcname="__hash__"):
|
|
249
|
+
fields = get_attributes(cls)
|
|
250
|
+
vals = ", ".join(
|
|
251
|
+
f"self.{name}"
|
|
252
|
+
for name, attrib in fields.items()
|
|
253
|
+
if attrib.compare
|
|
254
|
+
)
|
|
255
|
+
code = f"def {funcname}(self): return hash(({vals}))\n"
|
|
256
|
+
globs = {}
|
|
257
|
+
return GeneratedCode(code, globs)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
init_maker = MethodMaker("__init__", init_generator)
|
|
261
|
+
prefab_init_maker = MethodMaker(PREFAB_INIT_FUNC, init_generator)
|
|
262
|
+
repr_maker = MethodMaker(
|
|
263
|
+
"__repr__",
|
|
264
|
+
get_repr_generator(recursion_safe=False, eval_safe=True)
|
|
265
|
+
)
|
|
266
|
+
recursive_repr_maker = MethodMaker(
|
|
267
|
+
"__repr__",
|
|
268
|
+
get_repr_generator(recursion_safe=True, eval_safe=True)
|
|
269
|
+
)
|
|
270
|
+
iter_maker = MethodMaker("__iter__", iter_generator)
|
|
271
|
+
asdict_maker = MethodMaker("as_dict", as_dict_generator)
|
|
272
|
+
hash_maker = MethodMaker("__hash__", hash_generator)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# Updated field with additional attributes
|
|
276
|
+
class Attribute(Field, ignore_annotations=True):
|
|
277
|
+
"""
|
|
278
|
+
Get an object to define a prefab attribute
|
|
279
|
+
|
|
280
|
+
:param default: Default value for this attribute
|
|
281
|
+
:param default_factory: 0 argument callable to give a default value
|
|
282
|
+
(for otherwise mutable defaults, eg: list)
|
|
283
|
+
:param init: Include this attribute in the __init__ parameters
|
|
284
|
+
:param repr: Include this attribute in the class __repr__
|
|
285
|
+
:param compare: Include this attribute in the class __eq__
|
|
286
|
+
:param iter: Include this attribute in the class __iter__ if generated
|
|
287
|
+
:param kw_only: Make this argument keyword only in init
|
|
288
|
+
:param serialize: Include this attribute in methods that serialize to dict
|
|
289
|
+
:param doc: Parameter documentation for slotted classes
|
|
290
|
+
:param metadata: Additional non-construction related metadata
|
|
291
|
+
:param type: Type of this attribute (for slotted classes)
|
|
292
|
+
"""
|
|
293
|
+
iter: bool = Field(default=True) # type: ignore
|
|
294
|
+
serialize: bool = Field(default=True) # type: ignore
|
|
295
|
+
metadata: dict = Field(default=FIELD_NOTHING, default_factory=dict) # type: ignore
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# noinspection PyShadowingBuiltins
|
|
299
|
+
def attribute(
|
|
300
|
+
*,
|
|
301
|
+
default=NOTHING,
|
|
302
|
+
default_factory=NOTHING,
|
|
303
|
+
init=True,
|
|
304
|
+
repr=True,
|
|
305
|
+
compare=True,
|
|
306
|
+
iter=True,
|
|
307
|
+
kw_only=False,
|
|
308
|
+
serialize=True,
|
|
309
|
+
exclude_field=False,
|
|
310
|
+
private=False,
|
|
311
|
+
doc=None,
|
|
312
|
+
metadata=None,
|
|
313
|
+
type=NOTHING,
|
|
314
|
+
):
|
|
315
|
+
"""
|
|
316
|
+
Helper function to get an object to define a prefab Attribute
|
|
317
|
+
|
|
318
|
+
:param default: Default value for this attribute
|
|
319
|
+
:param default_factory: 0 argument callable to give a default value
|
|
320
|
+
(for otherwise mutable defaults, eg: list)
|
|
321
|
+
:param init: Include this attribute in the __init__ parameters
|
|
322
|
+
:param repr: Include this attribute in the class __repr__
|
|
323
|
+
:param compare: Include this attribute in the class __eq__
|
|
324
|
+
:param iter: Include this attribute in the class __iter__ if generated
|
|
325
|
+
:param kw_only: Make this argument keyword only in init
|
|
326
|
+
:param serialize: Include this attribute in methods that serialize to dict
|
|
327
|
+
:param exclude_field: Shorthand for setting repr, compare, iter and serialize to False
|
|
328
|
+
:param private: Short for init, repr, compare, iter, serialize = False, must have default or factory
|
|
329
|
+
:param doc: Parameter documentation for slotted classes
|
|
330
|
+
:param metadata: Dictionary for additional non-construction metadata
|
|
331
|
+
:param type: Type of this attribute
|
|
332
|
+
|
|
333
|
+
:return: Attribute generated with these parameters.
|
|
334
|
+
"""
|
|
335
|
+
if exclude_field:
|
|
336
|
+
repr = False
|
|
337
|
+
compare = False
|
|
338
|
+
iter = False
|
|
339
|
+
serialize = False
|
|
340
|
+
|
|
341
|
+
if private:
|
|
342
|
+
if default is NOTHING and default_factory is NOTHING:
|
|
343
|
+
raise AttributeError("Private attributes must have defaults or factories.")
|
|
344
|
+
init = False
|
|
345
|
+
repr = False
|
|
346
|
+
compare = False
|
|
347
|
+
iter = False
|
|
348
|
+
serialize = False
|
|
349
|
+
|
|
350
|
+
return Attribute(
|
|
351
|
+
default=default,
|
|
352
|
+
default_factory=default_factory,
|
|
353
|
+
init=init,
|
|
354
|
+
repr=repr,
|
|
355
|
+
compare=compare,
|
|
356
|
+
iter=iter,
|
|
357
|
+
kw_only=kw_only,
|
|
358
|
+
serialize=serialize,
|
|
359
|
+
doc=doc,
|
|
360
|
+
type=type,
|
|
361
|
+
metadata=metadata,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
prefab_gatherer = make_unified_gatherer(
|
|
366
|
+
Attribute,
|
|
367
|
+
leave_default_values=False,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# Class Builders
|
|
372
|
+
# noinspection PyShadowingBuiltins
|
|
373
|
+
def _make_prefab(
|
|
374
|
+
cls,
|
|
375
|
+
*,
|
|
376
|
+
init=True,
|
|
377
|
+
repr=True,
|
|
378
|
+
eq=True,
|
|
379
|
+
iter=False,
|
|
380
|
+
match_args=True,
|
|
381
|
+
kw_only=False,
|
|
382
|
+
frozen=False,
|
|
383
|
+
replace=True,
|
|
384
|
+
dict_method=False,
|
|
385
|
+
recursive_repr=False,
|
|
386
|
+
gathered_fields=None,
|
|
387
|
+
ignore_annotations=False,
|
|
388
|
+
):
|
|
389
|
+
"""
|
|
390
|
+
Generate boilerplate code for dunder methods in a class.
|
|
391
|
+
|
|
392
|
+
:param cls: Class to convert to a prefab
|
|
393
|
+
:param init: generate __init__
|
|
394
|
+
:param repr: generate __repr__
|
|
395
|
+
:param eq: generate __eq__
|
|
396
|
+
:param iter: generate __iter__
|
|
397
|
+
:param match_args: generate __match_args__
|
|
398
|
+
:param kw_only: Make all attributes keyword only
|
|
399
|
+
:param frozen: Prevent attribute values from being changed once defined
|
|
400
|
+
(This does not prevent the modification of mutable attributes
|
|
401
|
+
such as lists)
|
|
402
|
+
:param replace: Add a generated __replace__ method
|
|
403
|
+
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
404
|
+
:param recursive_repr: Safely handle repr in case of recursion
|
|
405
|
+
:param gathered_fields: Pre-gathered fields callable, to skip re-collecting attributes
|
|
406
|
+
:param ignore_annotations: Ignore annotated fields and only look at `attribute` fields
|
|
407
|
+
:return: class with __ methods defined
|
|
408
|
+
"""
|
|
409
|
+
cls_dict = cls.__dict__
|
|
410
|
+
|
|
411
|
+
if build_completed(cls_dict):
|
|
412
|
+
raise PrefabError(
|
|
413
|
+
f"Decorated class {cls.__name__!r} "
|
|
414
|
+
f"has already been processed as a Prefab."
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if not frozen:
|
|
418
|
+
# If the class is not frozen, make sure it doesn't inherit
|
|
419
|
+
# from a frozen class
|
|
420
|
+
for base in cls.__mro__[1:-1]: # Exclude this class and object
|
|
421
|
+
try:
|
|
422
|
+
fields = get_flags(base)
|
|
423
|
+
except (TypeError, KeyError):
|
|
424
|
+
continue
|
|
425
|
+
else:
|
|
426
|
+
if fields.get("frozen") is True:
|
|
427
|
+
raise TypeError("Cannot inherit non-frozen prefab from a frozen one")
|
|
428
|
+
|
|
429
|
+
slots = cls_dict.get("__slots__")
|
|
430
|
+
slotted = False if slots is None else True
|
|
431
|
+
|
|
432
|
+
if gathered_fields is None:
|
|
433
|
+
gatherer = prefab_gatherer
|
|
434
|
+
else:
|
|
435
|
+
gatherer = gathered_fields
|
|
436
|
+
|
|
437
|
+
methods = set()
|
|
438
|
+
|
|
439
|
+
if init and "__init__" not in cls_dict:
|
|
440
|
+
methods.add(init_maker)
|
|
441
|
+
else:
|
|
442
|
+
methods.add(prefab_init_maker)
|
|
443
|
+
|
|
444
|
+
if repr and "__repr__" not in cls_dict:
|
|
445
|
+
if recursive_repr:
|
|
446
|
+
methods.add(recursive_repr_maker)
|
|
447
|
+
else:
|
|
448
|
+
methods.add(repr_maker)
|
|
449
|
+
if eq and "__eq__" not in cls_dict:
|
|
450
|
+
methods.add(eq_maker)
|
|
451
|
+
if iter and "__iter__" not in cls_dict:
|
|
452
|
+
methods.add(iter_maker)
|
|
453
|
+
if frozen:
|
|
454
|
+
# Check __setattr__ and __delattr__ are not already defined on this class
|
|
455
|
+
if "__setattr__" in cls_dict:
|
|
456
|
+
raise TypeError("Cannot overwrite '__setattr__' method that already exists")
|
|
457
|
+
elif "__delattr__" in cls_dict:
|
|
458
|
+
raise TypeError("Cannot overwrite '__delattr__' method that already exists")
|
|
459
|
+
methods.add(frozen_setattr_maker)
|
|
460
|
+
methods.add(frozen_delattr_maker)
|
|
461
|
+
if "__hash__" not in cls_dict: # it's ok if the user has defined __hash__ already
|
|
462
|
+
methods.add(hash_maker)
|
|
463
|
+
if dict_method:
|
|
464
|
+
methods.add(asdict_maker)
|
|
465
|
+
|
|
466
|
+
if replace and "__replace__" not in cls_dict:
|
|
467
|
+
methods.add(replace_maker)
|
|
468
|
+
|
|
469
|
+
flags = {
|
|
470
|
+
"slotted": slotted,
|
|
471
|
+
"init": init,
|
|
472
|
+
"repr": repr,
|
|
473
|
+
"eq": eq,
|
|
474
|
+
"iter": iter,
|
|
475
|
+
"match_args": match_args,
|
|
476
|
+
"kw_only": kw_only,
|
|
477
|
+
"frozen": frozen,
|
|
478
|
+
"replace": replace,
|
|
479
|
+
"dict_method": dict_method,
|
|
480
|
+
"recursive_repr": recursive_repr,
|
|
481
|
+
"ignore_annotations": ignore_annotations,
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
cls = builder(
|
|
485
|
+
cls,
|
|
486
|
+
gatherer=gatherer,
|
|
487
|
+
methods=methods,
|
|
488
|
+
flags=flags,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Get fields now the class has been built
|
|
492
|
+
fields = get_fields(cls)
|
|
493
|
+
|
|
494
|
+
# Check pre_init and post_init functions if they exist
|
|
495
|
+
try:
|
|
496
|
+
func = getattr(cls, PRE_INIT_FUNC)
|
|
497
|
+
func_code = func.__code__
|
|
498
|
+
except AttributeError:
|
|
499
|
+
pass
|
|
500
|
+
else:
|
|
501
|
+
if func_code.co_posonlyargcount > 0:
|
|
502
|
+
raise PrefabError(
|
|
503
|
+
"Positional only arguments are not supported in pre or post init functions."
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
argcount = func_code.co_argcount + func_code.co_kwonlyargcount
|
|
507
|
+
|
|
508
|
+
# Include the first argument if the method is static
|
|
509
|
+
is_static = type(cls.__dict__.get(PRE_INIT_FUNC)) is staticmethod
|
|
510
|
+
|
|
511
|
+
arglist = (
|
|
512
|
+
func_code.co_varnames[:argcount]
|
|
513
|
+
if is_static
|
|
514
|
+
else func_code.co_varnames[1:argcount]
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
for item in arglist:
|
|
518
|
+
if item not in fields.keys():
|
|
519
|
+
raise PrefabError(
|
|
520
|
+
f"{item} argument in {PRE_INIT_FUNC} is not a valid attribute."
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
post_init_args = []
|
|
524
|
+
try:
|
|
525
|
+
func = getattr(cls, POST_INIT_FUNC)
|
|
526
|
+
func_code = func.__code__
|
|
527
|
+
except AttributeError:
|
|
528
|
+
pass
|
|
529
|
+
else:
|
|
530
|
+
if func_code.co_posonlyargcount > 0:
|
|
531
|
+
raise PrefabError(
|
|
532
|
+
"Positional only arguments are not supported in pre or post init functions."
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
argcount = func_code.co_argcount + func_code.co_kwonlyargcount
|
|
536
|
+
|
|
537
|
+
# Include the first argument if the method is static
|
|
538
|
+
is_static = type(cls.__dict__.get(POST_INIT_FUNC)) is staticmethod
|
|
539
|
+
|
|
540
|
+
arglist = (
|
|
541
|
+
func_code.co_varnames[:argcount]
|
|
542
|
+
if is_static
|
|
543
|
+
else func_code.co_varnames[1:argcount]
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
for item in arglist:
|
|
547
|
+
if item not in fields.keys():
|
|
548
|
+
raise PrefabError(
|
|
549
|
+
f"{item} argument in {POST_INIT_FUNC} is not a valid attribute."
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
post_init_args.extend(arglist)
|
|
553
|
+
|
|
554
|
+
# Gather values for match_args and do some syntax checking
|
|
555
|
+
default_defined = []
|
|
556
|
+
valid_args = list(fields.keys())
|
|
557
|
+
|
|
558
|
+
for name, attrib in fields.items():
|
|
559
|
+
# slot_gather and parent classes may use Fields
|
|
560
|
+
# prefabs require Attributes, so convert.
|
|
561
|
+
if not isinstance(attrib, Attribute):
|
|
562
|
+
attrib = Attribute.from_field(attrib)
|
|
563
|
+
fields[name] = attrib
|
|
564
|
+
|
|
565
|
+
if not kw_only:
|
|
566
|
+
# Syntax check arguments for __init__ don't have non-default after default
|
|
567
|
+
if attrib.init and not attrib.kw_only:
|
|
568
|
+
if attrib.default is not NOTHING or attrib.default_factory is not NOTHING:
|
|
569
|
+
default_defined.append(name)
|
|
570
|
+
else:
|
|
571
|
+
if default_defined:
|
|
572
|
+
names = ", ".join(default_defined)
|
|
573
|
+
raise SyntaxError(
|
|
574
|
+
"non-default argument follows default argument",
|
|
575
|
+
f"defaults: {names}",
|
|
576
|
+
f"non_default after default: {name}",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
setattr(cls, PREFAB_FIELDS, valid_args)
|
|
580
|
+
|
|
581
|
+
if match_args and "__match_args__" not in cls_dict:
|
|
582
|
+
setattr(cls, "__match_args__", tuple(valid_args))
|
|
583
|
+
|
|
584
|
+
return cls
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
class Prefab(metaclass=SlotMakerMeta, gatherer=prefab_gatherer):
|
|
588
|
+
__slots__ = {} # type: ignore
|
|
589
|
+
|
|
590
|
+
def __init_subclass__(
|
|
591
|
+
cls,
|
|
592
|
+
**kwargs
|
|
593
|
+
):
|
|
594
|
+
"""
|
|
595
|
+
Generate boilerplate code for dunder methods in a class.
|
|
596
|
+
|
|
597
|
+
Use as a base class, slotted by default
|
|
598
|
+
|
|
599
|
+
:param init: generates __init__ if true or __prefab_init__ if false
|
|
600
|
+
:param repr: generate __repr__
|
|
601
|
+
:param eq: generate __eq__
|
|
602
|
+
:param iter: generate __iter__
|
|
603
|
+
:param match_args: generate __match_args__
|
|
604
|
+
:param kw_only: make all attributes keyword only
|
|
605
|
+
:param frozen: Prevent attribute values from being changed once defined
|
|
606
|
+
(This does not prevent the modification of mutable attributes such as lists)
|
|
607
|
+
:param replace: generate a __replace__ method
|
|
608
|
+
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
609
|
+
:param recursive_repr: Safely handle repr in case of recursion
|
|
610
|
+
:param ignore_annotations: Ignore type annotations when gathering fields, only look for
|
|
611
|
+
slots or attribute(...) values
|
|
612
|
+
:param slots: automatically generate slots for this class's attributes
|
|
613
|
+
"""
|
|
614
|
+
default_values = {
|
|
615
|
+
"init": True,
|
|
616
|
+
"repr": True,
|
|
617
|
+
"eq": True,
|
|
618
|
+
"iter": False,
|
|
619
|
+
"match_args": True,
|
|
620
|
+
"kw_only": False,
|
|
621
|
+
"frozen": False,
|
|
622
|
+
"replace": True,
|
|
623
|
+
"dict_method": False,
|
|
624
|
+
"recursive_repr": False,
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
try:
|
|
628
|
+
flags = get_flags(cls).copy()
|
|
629
|
+
except (TypeError, KeyError):
|
|
630
|
+
flags = {}
|
|
631
|
+
else:
|
|
632
|
+
# Remove the value of slotted if it exists
|
|
633
|
+
flags.pop("slotted", None)
|
|
634
|
+
|
|
635
|
+
for k in default_values:
|
|
636
|
+
kwarg_value = kwargs.pop(k, None)
|
|
637
|
+
default = default_values[k]
|
|
638
|
+
|
|
639
|
+
if kwarg_value is not None:
|
|
640
|
+
flags[k] = kwarg_value
|
|
641
|
+
elif flags.get(k) is None:
|
|
642
|
+
flags[k] = default
|
|
643
|
+
|
|
644
|
+
if kwargs:
|
|
645
|
+
error_args = ", ".join(repr(k) for k in kwargs)
|
|
646
|
+
raise TypeError(
|
|
647
|
+
f"Prefab.__init_subclass__ got unexpected keyword arguments {error_args}"
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
_make_prefab(
|
|
651
|
+
cls,
|
|
652
|
+
**flags
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# noinspection PyShadowingBuiltins
|
|
657
|
+
def prefab(
|
|
658
|
+
cls=None,
|
|
659
|
+
*,
|
|
660
|
+
init=True,
|
|
661
|
+
repr=True,
|
|
662
|
+
eq=True,
|
|
663
|
+
iter=False,
|
|
664
|
+
match_args=True,
|
|
665
|
+
kw_only=False,
|
|
666
|
+
frozen=False,
|
|
667
|
+
replace=True,
|
|
668
|
+
dict_method=False,
|
|
669
|
+
recursive_repr=False,
|
|
670
|
+
ignore_annotations=False,
|
|
671
|
+
):
|
|
672
|
+
"""
|
|
673
|
+
Generate boilerplate code for dunder methods in a class.
|
|
674
|
+
|
|
675
|
+
Use as a decorator.
|
|
676
|
+
|
|
677
|
+
:param cls: Class to convert to a prefab
|
|
678
|
+
:param init: generates __init__ if true or __prefab_init__ if false
|
|
679
|
+
:param repr: generate __repr__
|
|
680
|
+
:param eq: generate __eq__
|
|
681
|
+
:param iter: generate __iter__
|
|
682
|
+
:param match_args: generate __match_args__
|
|
683
|
+
:param kw_only: make all attributes keyword only
|
|
684
|
+
:param frozen: Prevent attribute values from being changed once defined
|
|
685
|
+
(This does not prevent the modification of mutable attributes such as lists)
|
|
686
|
+
:param replace: generate a __replace__ method
|
|
687
|
+
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
688
|
+
:param recursive_repr: Safely handle repr in case of recursion
|
|
689
|
+
:param ignore_annotations: Ignore type annotations when gathering fields, only look for
|
|
690
|
+
slots or attribute(...) values
|
|
691
|
+
|
|
692
|
+
:return: class with __ methods defined
|
|
693
|
+
"""
|
|
694
|
+
if not cls:
|
|
695
|
+
# Called as () method to change defaults
|
|
696
|
+
return lambda cls_: prefab(
|
|
697
|
+
cls_,
|
|
698
|
+
init=init,
|
|
699
|
+
repr=repr,
|
|
700
|
+
eq=eq,
|
|
701
|
+
iter=iter,
|
|
702
|
+
match_args=match_args,
|
|
703
|
+
kw_only=kw_only,
|
|
704
|
+
frozen=frozen,
|
|
705
|
+
replace=replace,
|
|
706
|
+
dict_method=dict_method,
|
|
707
|
+
recursive_repr=recursive_repr,
|
|
708
|
+
ignore_annotations=ignore_annotations,
|
|
709
|
+
)
|
|
710
|
+
else:
|
|
711
|
+
return _make_prefab(
|
|
712
|
+
cls,
|
|
713
|
+
init=init,
|
|
714
|
+
repr=repr,
|
|
715
|
+
eq=eq,
|
|
716
|
+
iter=iter,
|
|
717
|
+
match_args=match_args,
|
|
718
|
+
kw_only=kw_only,
|
|
719
|
+
frozen=frozen,
|
|
720
|
+
replace=replace,
|
|
721
|
+
dict_method=dict_method,
|
|
722
|
+
recursive_repr=recursive_repr,
|
|
723
|
+
ignore_annotations=ignore_annotations,
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
# noinspection PyShadowingBuiltins
|
|
728
|
+
def build_prefab(
|
|
729
|
+
class_name,
|
|
730
|
+
attributes,
|
|
731
|
+
*,
|
|
732
|
+
bases=(),
|
|
733
|
+
class_dict=None,
|
|
734
|
+
init=True,
|
|
735
|
+
repr=True,
|
|
736
|
+
eq=True,
|
|
737
|
+
iter=False,
|
|
738
|
+
match_args=True,
|
|
739
|
+
kw_only=False,
|
|
740
|
+
frozen=False,
|
|
741
|
+
replace=True,
|
|
742
|
+
dict_method=False,
|
|
743
|
+
recursive_repr=False,
|
|
744
|
+
slots=False,
|
|
745
|
+
):
|
|
746
|
+
"""
|
|
747
|
+
Dynamically construct a (dynamic) prefab.
|
|
748
|
+
|
|
749
|
+
:param class_name: name of the resulting prefab class
|
|
750
|
+
:param attributes: list of (name, attribute()) pairs to assign to the class
|
|
751
|
+
for construction
|
|
752
|
+
:param bases: Base classes to inherit from
|
|
753
|
+
:param class_dict: Other values to add to the class dictionary on creation
|
|
754
|
+
This is the 'dict' parameter from 'type'
|
|
755
|
+
:param init: generates __init__ if true or __prefab_init__ if false
|
|
756
|
+
:param repr: generate __repr__
|
|
757
|
+
:param eq: generate __eq__
|
|
758
|
+
:param iter: generate __iter__
|
|
759
|
+
:param match_args: generate __match_args__
|
|
760
|
+
:param kw_only: make all attributes keyword only
|
|
761
|
+
:param frozen: Prevent attribute values from being changed once defined
|
|
762
|
+
(This does not prevent the modification of mutable attributes such as lists)
|
|
763
|
+
:param replace: generate a __replace__ method
|
|
764
|
+
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
765
|
+
:param recursive_repr: Safely handle repr in case of recursion
|
|
766
|
+
:param slots: Make the resulting class slotted
|
|
767
|
+
:return: class with __ methods defined
|
|
768
|
+
"""
|
|
769
|
+
class_dict = {} if class_dict is None else class_dict.copy()
|
|
770
|
+
|
|
771
|
+
class_annotations = {}
|
|
772
|
+
class_slots = {}
|
|
773
|
+
fields = {}
|
|
774
|
+
|
|
775
|
+
for name, attrib in attributes:
|
|
776
|
+
if isinstance(attrib, Attribute):
|
|
777
|
+
fields[name] = attrib
|
|
778
|
+
elif isinstance(attrib, Field):
|
|
779
|
+
fields[name] = Attribute.from_field(attrib)
|
|
780
|
+
else:
|
|
781
|
+
fields[name] = Attribute(default=attrib)
|
|
782
|
+
|
|
783
|
+
if attrib.type is not NOTHING:
|
|
784
|
+
class_annotations[name] = attrib.type
|
|
785
|
+
|
|
786
|
+
class_slots[name] = attrib.doc
|
|
787
|
+
|
|
788
|
+
if slots:
|
|
789
|
+
class_dict["__slots__"] = class_slots
|
|
790
|
+
|
|
791
|
+
class_dict["__annotations__"] = class_annotations
|
|
792
|
+
|
|
793
|
+
cls = type(class_name, bases, class_dict)
|
|
794
|
+
|
|
795
|
+
gathered_fields = GatheredFields(fields, {})
|
|
796
|
+
|
|
797
|
+
cls = _make_prefab(
|
|
798
|
+
cls,
|
|
799
|
+
init=init,
|
|
800
|
+
repr=repr,
|
|
801
|
+
eq=eq,
|
|
802
|
+
iter=iter,
|
|
803
|
+
match_args=match_args,
|
|
804
|
+
kw_only=kw_only,
|
|
805
|
+
frozen=frozen,
|
|
806
|
+
replace=replace,
|
|
807
|
+
dict_method=dict_method,
|
|
808
|
+
recursive_repr=recursive_repr,
|
|
809
|
+
gathered_fields=gathered_fields,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
return cls
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
# Extra Functions
|
|
816
|
+
def is_prefab(o):
|
|
817
|
+
"""
|
|
818
|
+
Identifier function, return True if an object is a prefab class *or* if
|
|
819
|
+
it is an instance of a prefab class.
|
|
820
|
+
|
|
821
|
+
The check works by looking for a PREFAB_FIELDS attribute.
|
|
822
|
+
|
|
823
|
+
:param o: object for comparison
|
|
824
|
+
:return: True/False
|
|
825
|
+
"""
|
|
826
|
+
cls = o if isinstance(o, type) else type(o)
|
|
827
|
+
return hasattr(cls, PREFAB_FIELDS)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def is_prefab_instance(o):
|
|
831
|
+
"""
|
|
832
|
+
Identifier function, return True if an object is an instance of a prefab
|
|
833
|
+
class.
|
|
834
|
+
|
|
835
|
+
The check works by looking for a PREFAB_FIELDS attribute.
|
|
836
|
+
|
|
837
|
+
:param o: object for comparison
|
|
838
|
+
:return: True/False
|
|
839
|
+
"""
|
|
840
|
+
return hasattr(type(o), PREFAB_FIELDS)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def as_dict(o):
|
|
844
|
+
"""
|
|
845
|
+
Get the valid fields from a prefab respecting the serialize
|
|
846
|
+
values of attributes
|
|
847
|
+
|
|
848
|
+
:param o: instance of a prefab class
|
|
849
|
+
:return: dictionary of {k: v} from fields
|
|
850
|
+
"""
|
|
851
|
+
cls = type(o)
|
|
852
|
+
if not hasattr(cls, PREFAB_FIELDS):
|
|
853
|
+
raise TypeError(f"{o!r} should be a prefab instance, not {cls}")
|
|
854
|
+
|
|
855
|
+
# Attempt to use the generated method if available
|
|
856
|
+
try:
|
|
857
|
+
return o.as_dict()
|
|
858
|
+
except AttributeError:
|
|
859
|
+
pass
|
|
860
|
+
|
|
861
|
+
flds = get_attributes(cls)
|
|
862
|
+
|
|
863
|
+
return {
|
|
864
|
+
name: getattr(o, name)
|
|
865
|
+
for name, attrib in flds.items()
|
|
866
|
+
if attrib.serialize
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def replace(obj, /, **changes):
|
|
871
|
+
"""
|
|
872
|
+
Create a copy of a prefab instance with values provided to 'changes' replaced
|
|
873
|
+
|
|
874
|
+
:param obj: prefab instance
|
|
875
|
+
:return: new prefab instance
|
|
876
|
+
"""
|
|
877
|
+
if not is_prefab_instance(obj):
|
|
878
|
+
raise TypeError("replace() should be called on prefab instances")
|
|
879
|
+
try:
|
|
880
|
+
replace_func = obj.__replace__
|
|
881
|
+
except AttributeError:
|
|
882
|
+
raise TypeError(f"{obj.__class__.__name__!r} does not support __replace__")
|
|
883
|
+
|
|
884
|
+
return replace_func(**changes)
|