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