ducktools-classbuilder 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

@@ -25,33 +25,25 @@ A 'prebuilt' implementation of class generation.
25
25
 
26
26
  Includes pre and post init functions along with other methods.
27
27
  """
28
-
29
- import sys
30
-
31
28
  from . import (
32
29
  INTERNALS_DICT, NOTHING,
33
- Field, MethodMaker, SlotFields,
34
- builder, fieldclass, get_flags, get_fields, make_slot_gatherer,
35
- frozen_setattr_maker, frozen_delattr_maker, is_classvar,
30
+ Field, MethodMaker, GatheredFields, GeneratedCode, SlotMakerMeta,
31
+ builder, get_flags, get_fields,
32
+ make_unified_gatherer,
33
+ frozen_setattr_maker, frozen_delattr_maker, eq_maker,
34
+ get_repr_generator,
36
35
  )
37
36
 
37
+ # These aren't used but are re-exported for ease of use
38
+ # noinspection PyUnresolvedReferences
39
+ from . import SlotFields, KW_ONLY
40
+
38
41
  PREFAB_FIELDS = "PREFAB_FIELDS"
39
42
  PREFAB_INIT_FUNC = "__prefab_init__"
40
43
  PRE_INIT_FUNC = "__prefab_pre_init__"
41
44
  POST_INIT_FUNC = "__prefab_post_init__"
42
45
 
43
46
 
44
- # KW_ONLY sentinel 'type' to use to indicate all subsequent attributes are
45
- # keyword only
46
- # noinspection PyPep8Naming
47
- class _KW_ONLY_TYPE:
48
- def __repr__(self):
49
- return "<KW_ONLY Sentinel Object>"
50
-
51
-
52
- KW_ONLY = _KW_ONLY_TYPE()
53
-
54
-
55
47
  class PrefabError(Exception):
56
48
  pass
57
49
 
@@ -69,7 +61,7 @@ def get_attributes(cls):
69
61
 
70
62
  # Method Generators
71
63
  def get_init_maker(*, init_name="__init__"):
72
- def __init__(cls: "type") -> "tuple[str, dict]":
64
+ def __init__(cls: type) -> GeneratedCode:
73
65
  globs = {}
74
66
  # Get the internals dictionary and prepare attributes
75
67
  attributes = get_attributes(cls)
@@ -218,100 +210,18 @@ def get_init_maker(*, init_name="__init__"):
218
210
  f"{body}\n"
219
211
  f"{post_init_call}\n"
220
212
  )
221
- return code, globs
213
+ return GeneratedCode(code, globs)
222
214
 
223
215
  return MethodMaker(init_name, __init__)
224
216
 
225
217
 
226
- def get_repr_maker(*, recursion_safe=False):
227
- def __repr__(cls: "type") -> "tuple[str, dict]":
228
- attributes = get_attributes(cls)
229
-
230
- globs = {}
231
-
232
- will_eval = True
233
- valid_names = []
234
- for name, attrib in attributes.items():
235
- if attrib.repr and not attrib.exclude_field:
236
- valid_names.append(name)
237
-
238
- # If the init fields don't match the repr, or some fields are excluded
239
- # generate a repr that clearly will not evaluate
240
- if will_eval and (attrib.exclude_field or (attrib.init ^ attrib.repr)):
241
- will_eval = False
242
-
243
- content = ", ".join(
244
- f"{name}={{self.{name}!r}}"
245
- for name in valid_names
246
- )
247
-
248
- if recursion_safe:
249
- import reprlib
250
- globs["_recursive_repr"] = reprlib.recursive_repr()
251
- recursion_func = "@_recursive_repr\n"
252
- else:
253
- recursion_func = ""
254
-
255
- if will_eval:
256
- code = (
257
- f"{recursion_func}"
258
- f"def __repr__(self):\n"
259
- f" return f'{{type(self).__qualname__}}({content})'\n"
260
- )
261
- else:
262
- if content:
263
- code = (
264
- f"{recursion_func}"
265
- f"def __repr__(self):\n"
266
- f" return f'<prefab {{type(self).__qualname__}}; {content}>'\n"
267
- )
268
- else:
269
- code = (
270
- f"{recursion_func}"
271
- f"def __repr__(self):\n"
272
- f" return f'<prefab {{type(self).__qualname__}}>'\n"
273
- )
274
-
275
- return code, globs
276
-
277
- return MethodMaker("__repr__", __repr__)
278
-
279
-
280
- def get_eq_maker():
281
- def __eq__(cls: "type") -> "tuple[str, dict]":
282
- class_comparison = "self.__class__ is other.__class__"
283
- attribs = get_attributes(cls)
284
- field_names = [
285
- name
286
- for name, attrib in attribs.items()
287
- if attrib.compare and not attrib.exclude_field
288
- ]
289
-
290
- if field_names:
291
- selfvals = ",".join(f"self.{name}" for name in field_names)
292
- othervals = ",".join(f"other.{name}" for name in field_names)
293
- instance_comparison = f"({selfvals},) == ({othervals},)"
294
- else:
295
- instance_comparison = "True"
296
-
297
- code = (
298
- f"def __eq__(self, other):\n"
299
- f" return {instance_comparison} if {class_comparison} else NotImplemented\n"
300
- )
301
- globs = {}
302
-
303
- return code, globs
304
-
305
- return MethodMaker("__eq__", __eq__)
306
-
307
-
308
218
  def get_iter_maker():
309
- def __iter__(cls: "type") -> "tuple[str, dict]":
219
+ def __iter__(cls: type) -> GeneratedCode:
310
220
  fields = get_attributes(cls)
311
221
 
312
222
  valid_fields = (
313
223
  name for name, attrib in fields.items()
314
- if attrib.iter and not attrib.exclude_field
224
+ if attrib.iter
315
225
  )
316
226
 
317
227
  values = "\n".join(f" yield self.{name}" for name in valid_fields)
@@ -322,56 +232,61 @@ def get_iter_maker():
322
232
 
323
233
  code = f"def __iter__(self):\n{values}"
324
234
  globs = {}
325
- return code, globs
235
+ return GeneratedCode(code, globs)
326
236
 
327
237
  return MethodMaker("__iter__", __iter__)
328
238
 
329
239
 
330
240
  def get_asdict_maker():
331
- def as_dict_gen(cls: "type") -> "tuple[str, dict]":
241
+ def as_dict_gen(cls: type) -> GeneratedCode:
332
242
  fields = get_attributes(cls)
333
243
 
334
244
  vals = ", ".join(
335
245
  f"'{name}': self.{name}"
336
246
  for name, attrib in fields.items()
337
- if attrib.serialize and not attrib.exclude_field
247
+ if attrib.serialize
338
248
  )
339
249
  out_dict = f"{{{vals}}}"
340
250
  code = f"def as_dict(self): return {out_dict}"
341
251
 
342
252
  globs = {}
343
- return code, globs
253
+ return GeneratedCode(code, globs)
344
254
  return MethodMaker("as_dict", as_dict_gen)
345
255
 
346
256
 
347
257
  init_maker = get_init_maker()
348
258
  prefab_init_maker = get_init_maker(init_name=PREFAB_INIT_FUNC)
349
- repr_maker = get_repr_maker()
350
- recursive_repr_maker = get_repr_maker(recursion_safe=True)
351
- eq_maker = get_eq_maker()
259
+ repr_maker = MethodMaker(
260
+ "__repr__",
261
+ get_repr_generator(recursion_safe=False, eval_safe=True)
262
+ )
263
+ recursive_repr_maker = MethodMaker(
264
+ "__repr__",
265
+ get_repr_generator(recursion_safe=True, eval_safe=True)
266
+ )
352
267
  iter_maker = get_iter_maker()
353
268
  asdict_maker = get_asdict_maker()
354
269
 
355
270
 
356
271
  # Updated field with additional attributes
357
- @fieldclass
358
272
  class Attribute(Field):
359
- __slots__ = SlotFields(
360
- init=True,
361
- repr=True,
362
- compare=True,
363
- iter=True,
364
- kw_only=False,
365
- serialize=True,
366
- exclude_field=False,
367
- )
273
+ """
274
+ Get an object to define a prefab attribute
368
275
 
369
- def validate_field(self):
370
- super().validate_field()
371
- if self.kw_only and not self.init:
372
- raise PrefabError(
373
- "Attribute cannot be keyword only if it is not in init."
374
- )
276
+ :param default: Default value for this attribute
277
+ :param default_factory: 0 argument callable to give a default value
278
+ (for otherwise mutable defaults, eg: list)
279
+ :param init: Include this attribute in the __init__ parameters
280
+ :param repr: Include this attribute in the class __repr__
281
+ :param compare: Include this attribute in the class __eq__
282
+ :param iter: Include this attribute in the class __iter__ if generated
283
+ :param kw_only: Make this argument keyword only in init
284
+ :param serialize: Include this attribute in methods that serialize to dict
285
+ :param doc: Parameter documentation for slotted classes
286
+ :param type: Type of this attribute (for slotted classes)
287
+ """
288
+ iter: bool = True
289
+ serialize: bool = True
375
290
 
376
291
 
377
292
  # noinspection PyShadowingBuiltins
@@ -390,7 +305,7 @@ def attribute(
390
305
  type=NOTHING,
391
306
  ):
392
307
  """
393
- Get an object to define a prefab Attribute
308
+ Helper function to get an object to define a prefab Attribute
394
309
 
395
310
  :param default: Default value for this attribute
396
311
  :param default_factory: 0 argument callable to give a default value
@@ -401,15 +316,18 @@ def attribute(
401
316
  :param iter: Include this attribute in the class __iter__ if generated
402
317
  :param kw_only: Make this argument keyword only in init
403
318
  :param serialize: Include this attribute in methods that serialize to dict
404
- :param exclude_field: Exclude this field from all magic method generation
405
- apart from __init__ signature
406
- and do not include it in PREFAB_FIELDS
407
- Must be assigned in __prefab_post_init__
319
+ :param exclude_field: Shorthand for setting repr, compare, iter and serialize to False
408
320
  :param doc: Parameter documentation for slotted classes
409
321
  :param type: Type of this attribute (for slotted classes)
410
322
 
411
323
  :return: Attribute generated with these parameters.
412
324
  """
325
+ if exclude_field:
326
+ repr = False
327
+ compare = False
328
+ iter = False
329
+ serialize = False
330
+
413
331
  return Attribute(
414
332
  default=default,
415
333
  default_factory=default_factory,
@@ -419,90 +337,12 @@ def attribute(
419
337
  iter=iter,
420
338
  kw_only=kw_only,
421
339
  serialize=serialize,
422
- exclude_field=exclude_field,
423
340
  doc=doc,
424
341
  type=type,
425
342
  )
426
343
 
427
344
 
428
- slot_prefab_gatherer = make_slot_gatherer(Attribute)
429
-
430
-
431
- # Gatherer for classes built on attributes or annotations
432
- def attribute_gatherer(cls):
433
- cls_annotations = cls.__dict__.get("__annotations__", {})
434
- cls_annotation_names = cls_annotations.keys()
435
-
436
- cls_slots = cls.__dict__.get("__slots__", {})
437
-
438
- cls_attributes = {
439
- k: v for k, v in vars(cls).items() if isinstance(v, Attribute)
440
- }
441
-
442
- cls_attribute_names = cls_attributes.keys()
443
-
444
- cls_modifications = {}
445
-
446
- if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
447
- # replace the classes' attributes dict with one with the correct
448
- # order from the annotations.
449
- kw_flag = False
450
- new_attributes = {}
451
- for name, value in cls_annotations.items():
452
- # Ignore ClassVar hints
453
- if is_classvar(value):
454
- continue
455
-
456
- # Look for the KW_ONLY annotation
457
- if value is KW_ONLY or value == "KW_ONLY":
458
- if kw_flag:
459
- raise PrefabError(
460
- "Class can not be defined as keyword only twice"
461
- )
462
- kw_flag = True
463
- else:
464
- # Copy attributes that are already defined to the new dict
465
- # generate Attribute() values for those that are not defined.
466
-
467
- # Extra parameters to pass to each Attribute
468
- extras = {
469
- "type": cls_annotations[name]
470
- }
471
- if kw_flag:
472
- extras["kw_only"] = True
473
-
474
- # If a field name is also declared in slots it can't have a real
475
- # default value and the attr will be the slot descriptor.
476
- if hasattr(cls, name) and name not in cls_slots:
477
- if name in cls_attribute_names:
478
- attrib = Attribute.from_field(
479
- cls_attributes[name],
480
- **extras,
481
- )
482
- else:
483
- attribute_default = getattr(cls, name)
484
- attrib = attribute(default=attribute_default, **extras)
485
-
486
- # Clear the attribute from the class after it has been used
487
- # in the definition.
488
- cls_modifications[name] = NOTHING
489
- else:
490
- attrib = attribute(**extras)
491
-
492
- new_attributes[name] = attrib
493
-
494
- cls_attributes = new_attributes
495
- else:
496
- for name in cls_attributes.keys():
497
- attrib = cls_attributes[name]
498
- cls_modifications[name] = NOTHING
499
-
500
- # Some items can still be annotated.
501
- if name in cls_annotations:
502
- new_attrib = Attribute.from_field(attrib, type=cls_annotations[name])
503
- cls_attributes[name] = new_attrib
504
-
505
- return cls_attributes, cls_modifications
345
+ prefab_gatherer = make_unified_gatherer(Attribute, False)
506
346
 
507
347
 
508
348
  # Class Builders
@@ -519,6 +359,7 @@ def _make_prefab(
519
359
  frozen=False,
520
360
  dict_method=False,
521
361
  recursive_repr=False,
362
+ gathered_fields=None,
522
363
  ):
523
364
  """
524
365
  Generate boilerplate code for dunder methods in a class.
@@ -535,6 +376,7 @@ def _make_prefab(
535
376
  such as lists)
536
377
  :param dict_method: Include an as_dict method for faster dictionary creation
537
378
  :param recursive_repr: Safely handle repr in case of recursion
379
+ :param gathered_fields: Pre-gathered fields callable, to skip re-collecting attributes
538
380
  :return: class with __ methods defined
539
381
  """
540
382
  cls_dict = cls.__dict__
@@ -546,12 +388,13 @@ def _make_prefab(
546
388
  )
547
389
 
548
390
  slots = cls_dict.get("__slots__")
549
- if isinstance(slots, SlotFields):
550
- gatherer = slot_prefab_gatherer
551
- slotted = True
391
+
392
+ slotted = False if slots is None else True
393
+
394
+ if gathered_fields is None:
395
+ gatherer = prefab_gatherer
552
396
  else:
553
- gatherer = attribute_gatherer
554
- slotted = False
397
+ gatherer = gathered_fields
555
398
 
556
399
  methods = set()
557
400
 
@@ -651,9 +494,9 @@ def _make_prefab(
651
494
  post_init_args.extend(arglist)
652
495
 
653
496
  # Gather values for match_args and do some syntax checking
654
-
655
497
  default_defined = []
656
- valid_args = []
498
+ valid_args = list(fields.keys())
499
+
657
500
  for name, attrib in fields.items():
658
501
  # slot_gather and parent classes may use Fields
659
502
  # prefabs require Attributes, so convert.
@@ -661,15 +504,6 @@ def _make_prefab(
661
504
  attrib = Attribute.from_field(attrib)
662
505
  fields[name] = attrib
663
506
 
664
- # Excluded fields *MUST* be forwarded to post_init
665
- if attrib.exclude_field:
666
- if name not in post_init_args:
667
- raise PrefabError(
668
- f"{name!r} is an excluded attribute but is not passed to post_init"
669
- )
670
- else:
671
- valid_args.append(name)
672
-
673
507
  if not kw_only:
674
508
  # Syntax check arguments for __init__ don't have non-default after default
675
509
  if attrib.init and not attrib.kw_only:
@@ -692,6 +526,37 @@ def _make_prefab(
692
526
  return cls
693
527
 
694
528
 
529
+ class Prefab(metaclass=SlotMakerMeta):
530
+ _meta_gatherer = prefab_gatherer
531
+ __slots__ = {}
532
+
533
+ # noinspection PyShadowingBuiltins
534
+ def __init_subclass__(
535
+ cls,
536
+ init=True,
537
+ repr=True,
538
+ eq=True,
539
+ iter=False,
540
+ match_args=True,
541
+ kw_only=False,
542
+ frozen=False,
543
+ dict_method=False,
544
+ recursive_repr=False,
545
+ ):
546
+ _make_prefab(
547
+ cls,
548
+ init=init,
549
+ repr=repr,
550
+ eq=eq,
551
+ iter=iter,
552
+ match_args=match_args,
553
+ kw_only=kw_only,
554
+ frozen=frozen,
555
+ dict_method=dict_method,
556
+ recursive_repr=recursive_repr,
557
+ )
558
+
559
+
695
560
  # noinspection PyShadowingBuiltins
696
561
  def prefab(
697
562
  cls=None,
@@ -770,6 +635,7 @@ def build_prefab(
770
635
  frozen=False,
771
636
  dict_method=False,
772
637
  recursive_repr=False,
638
+ slots=False,
773
639
  ):
774
640
  """
775
641
  Dynamically construct a (dynamic) prefab.
@@ -790,12 +656,35 @@ def build_prefab(
790
656
  (This does not prevent the modification of mutable attributes such as lists)
791
657
  :param dict_method: Include an as_dict method for faster dictionary creation
792
658
  :param recursive_repr: Safely handle repr in case of recursion
659
+ :param slots: Make the resulting class slotted
793
660
  :return: class with __ methods defined
794
661
  """
795
- class_dict = {} if class_dict is None else class_dict
796
- cls = type(class_name, bases, class_dict)
662
+ class_dict = {} if class_dict is None else class_dict.copy()
663
+
664
+ class_annotations = {}
665
+ class_slots = {}
666
+ fields = {}
667
+
797
668
  for name, attrib in attributes:
798
- setattr(cls, name, attrib)
669
+ if isinstance(attrib, Attribute):
670
+ fields[name] = attrib
671
+ elif isinstance(attrib, Field):
672
+ fields[name] = Attribute.from_field(attrib)
673
+ else:
674
+ fields[name] = Attribute(default=attrib)
675
+
676
+ if attrib.type is not NOTHING:
677
+ class_annotations[name] = attrib.type
678
+
679
+ class_slots[name] = attrib.doc
680
+
681
+ if slots:
682
+ class_dict["__slots__"] = class_slots
683
+
684
+ class_dict["__annotations__"] = class_annotations
685
+ cls = type(class_name, bases, class_dict)
686
+
687
+ gathered_fields = GatheredFields(fields, {})
799
688
 
800
689
  cls = _make_prefab(
801
690
  cls,
@@ -808,6 +697,7 @@ def build_prefab(
808
697
  frozen=frozen,
809
698
  dict_method=dict_method,
810
699
  recursive_repr=recursive_repr,
700
+ gathered_fields=gathered_fields,
811
701
  )
812
702
 
813
703
  return cls
@@ -864,5 +754,5 @@ def as_dict(o):
864
754
  return {
865
755
  name: getattr(o, name)
866
756
  for name, attrib in flds.items()
867
- if attrib.serialize and not attrib.exclude_field
757
+ if attrib.serialize
868
758
  }
@@ -1,14 +1,18 @@
1
1
  import typing
2
+ from types import MappingProxyType
2
3
  from typing_extensions import dataclass_transform
3
4
 
4
5
  from collections.abc import Callable
5
6
 
6
7
  from . import (
7
- INTERNALS_DICT, NOTHING,
8
- Field, MethodMaker, SlotFields as SlotFields,
9
- builder, fieldclass, get_flags, get_fields, make_slot_gatherer
8
+ NOTHING,
9
+ Field,
10
+ MethodMaker,
11
+ SlotMakerMeta,
10
12
  )
11
13
 
14
+ from . import SlotFields as SlotFields, KW_ONLY as KW_ONLY
15
+
12
16
  # noinspection PyUnresolvedReferences
13
17
  from . import _NothingType
14
18
 
@@ -17,12 +21,7 @@ PREFAB_INIT_FUNC: str
17
21
  PRE_INIT_FUNC: str
18
22
  POST_INIT_FUNC: str
19
23
 
20
-
21
- # noinspection PyPep8Naming
22
- class _KW_ONLY_TYPE:
23
- def __repr__(self) -> str: ...
24
-
25
- KW_ONLY: _KW_ONLY_TYPE
24
+ _CopiableMappings = dict[str, typing.Any] | MappingProxyType[str, typing.Any]
26
25
 
27
26
  class PrefabError(Exception): ...
28
27
 
@@ -30,10 +29,6 @@ def get_attributes(cls: type) -> dict[str, Attribute]: ...
30
29
 
31
30
  def get_init_maker(*, init_name: str="__init__") -> MethodMaker: ...
32
31
 
33
- def get_repr_maker(*, recursion_safe: bool = False) -> MethodMaker: ...
34
-
35
- def get_eq_maker() -> MethodMaker: ...
36
-
37
32
  def get_iter_maker() -> MethodMaker: ...
38
33
 
39
34
  def get_asdict_maker() -> MethodMaker: ...
@@ -50,13 +45,8 @@ asdict_maker: MethodMaker
50
45
  class Attribute(Field):
51
46
  __slots__: dict
52
47
 
53
- init: bool
54
- repr: bool
55
- compare: bool
56
48
  iter: bool
57
- kw_only: bool
58
49
  serialize: bool
59
- exclude_field: bool
60
50
 
61
51
  def __init__(
62
52
  self,
@@ -71,7 +61,6 @@ class Attribute(Field):
71
61
  iter: bool = True,
72
62
  kw_only: bool = False,
73
63
  serialize: bool = True,
74
- exclude_field: bool = False,
75
64
  ) -> None: ...
76
65
 
77
66
  def __repr__(self) -> str: ...
@@ -93,9 +82,7 @@ def attribute(
93
82
  exclude_field: bool = False,
94
83
  ) -> Attribute: ...
95
84
 
96
- def slot_prefab_gatherer(cls: type) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
97
-
98
- def attribute_gatherer(cls: type) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
85
+ def prefab_gatherer(cls_or_ns: type | MappingProxyType) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
99
86
 
100
87
  def _make_prefab(
101
88
  cls: type,
@@ -109,10 +96,28 @@ def _make_prefab(
109
96
  frozen: bool = False,
110
97
  dict_method: bool = False,
111
98
  recursive_repr: bool = False,
99
+ gathered_fields: Callable[[type], tuple[dict[str, Attribute], dict[str, typing.Any]]] | None = None,
112
100
  ) -> type: ...
113
101
 
114
102
  _T = typing.TypeVar("_T")
115
103
 
104
+ # noinspection PyUnresolvedReferences
105
+ @dataclass_transform(field_specifiers=(Attribute, attribute))
106
+ class Prefab(metaclass=SlotMakerMeta):
107
+ _meta_gatherer: Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]
108
+ def __init_subclass__(
109
+ cls,
110
+ init: bool = True,
111
+ repr: bool = True,
112
+ eq: bool = True,
113
+ iter: bool = False,
114
+ match_args: bool = True,
115
+ kw_only: bool = False,
116
+ frozen: bool = False,
117
+ dict_method: bool = False,
118
+ recursive_repr: bool = False,
119
+ ) -> None: ...
120
+
116
121
 
117
122
  # For some reason PyCharm can't see 'attribute'?!?
118
123
  # noinspection PyUnresolvedReferences
@@ -146,6 +151,7 @@ def build_prefab(
146
151
  frozen: bool = False,
147
152
  dict_method: bool = False,
148
153
  recursive_repr: bool = False,
154
+ slots: bool = False,
149
155
  ) -> type: ...
150
156
 
151
157
  def is_prefab(o: typing.Any) -> bool: ...