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.

@@ -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
+ }