ducktools-classbuilder 0.4.0__py3-none-any.whl → 0.5.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.

Potentially problematic release.


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

@@ -21,7 +21,7 @@
21
21
  # SOFTWARE.
22
22
  import sys
23
23
 
24
- __version__ = "v0.4.0"
24
+ __version__ = "v0.5.1"
25
25
 
26
26
  # Change this name if you make heavy modifications
27
27
  INTERNALS_DICT = "__classbuilder_internals__"
@@ -65,7 +65,7 @@ def _get_inst_fields(inst):
65
65
  }
66
66
 
67
67
 
68
- # As 'None' can be a meaningful default we need a sentinel value
68
+ # As 'None' can be a meaningful value we need a sentinel value
69
69
  # to use to show no value has been provided.
70
70
  class _NothingType:
71
71
  def __repr__(self):
@@ -109,7 +109,7 @@ class MethodMaker:
109
109
  return method.__get__(instance, cls)
110
110
 
111
111
 
112
- def get_init_maker(null=NOTHING, extra_code=None):
112
+ def get_init_generator(null=NOTHING, extra_code=None):
113
113
  def cls_init_maker(cls):
114
114
  fields = get_fields(cls)
115
115
  flags = get_flags(cls)
@@ -154,10 +154,10 @@ def get_init_maker(null=NOTHING, extra_code=None):
154
154
  return cls_init_maker
155
155
 
156
156
 
157
- init_maker = get_init_maker()
157
+ init_generator = get_init_generator()
158
158
 
159
159
 
160
- def repr_maker(cls):
160
+ def repr_generator(cls):
161
161
  fields = get_fields(cls)
162
162
  content = ", ".join(
163
163
  f"{name}={{self.{name}!r}}"
@@ -171,7 +171,7 @@ def repr_maker(cls):
171
171
  return code, globs
172
172
 
173
173
 
174
- def eq_maker(cls):
174
+ def eq_generator(cls):
175
175
  class_comparison = "self.__class__ is other.__class__"
176
176
  field_names = get_fields(cls)
177
177
 
@@ -191,7 +191,7 @@ def eq_maker(cls):
191
191
  return code, globs
192
192
 
193
193
 
194
- def frozen_setattr_maker(cls):
194
+ def frozen_setattr_generator(cls):
195
195
  globs = {}
196
196
  field_names = set(get_fields(cls))
197
197
  flags = get_flags(cls)
@@ -220,7 +220,7 @@ def frozen_setattr_maker(cls):
220
220
  return code, globs
221
221
 
222
222
 
223
- def frozen_delattr_maker(cls):
223
+ def frozen_delattr_generator(cls):
224
224
  body = (
225
225
  ' raise TypeError(\n'
226
226
  ' f"{type(self).__name__!r} object "\n'
@@ -234,12 +234,12 @@ def frozen_delattr_maker(cls):
234
234
 
235
235
  # As only the __get__ method refers to the class we can use the same
236
236
  # Descriptor instances for every class.
237
- init_desc = MethodMaker("__init__", init_maker)
238
- repr_desc = MethodMaker("__repr__", repr_maker)
239
- eq_desc = MethodMaker("__eq__", eq_maker)
240
- frozen_setattr_desc = MethodMaker("__setattr__", frozen_setattr_maker)
241
- frozen_delattr_desc = MethodMaker("__delattr__", frozen_delattr_maker)
242
- default_methods = frozenset({init_desc, repr_desc, eq_desc})
237
+ init_maker = MethodMaker("__init__", init_generator)
238
+ repr_maker = MethodMaker("__repr__", repr_generator)
239
+ eq_maker = MethodMaker("__eq__", eq_generator)
240
+ frozen_setattr_maker = MethodMaker("__setattr__", frozen_setattr_generator)
241
+ frozen_delattr_maker = MethodMaker("__delattr__", frozen_delattr_generator)
242
+ default_methods = frozenset({init_maker, repr_maker, eq_maker})
243
243
 
244
244
 
245
245
  def builder(cls=None, /, *, gatherer, methods, flags=None):
@@ -248,7 +248,7 @@ def builder(cls=None, /, *, gatherer, methods, flags=None):
248
248
 
249
249
  :param cls: Class to be analysed and have methods generated
250
250
  :param gatherer: Function to gather field information
251
- :type gatherer: Callable[[type], dict[str, Field]]
251
+ :type gatherer: Callable[[type], tuple[dict[str, Field], dict[str, Any]]]
252
252
  :param methods: MethodMakers to add to the class
253
253
  :type methods: set[MethodMaker]
254
254
  :param flags: additional flags to store in the internals dictionary
@@ -267,7 +267,14 @@ def builder(cls=None, /, *, gatherer, methods, flags=None):
267
267
  internals = {}
268
268
  setattr(cls, INTERNALS_DICT, internals)
269
269
 
270
- cls_fields = gatherer(cls)
270
+ cls_fields, modifications = gatherer(cls)
271
+
272
+ for name, value in modifications.items():
273
+ if value is NOTHING:
274
+ delattr(cls, name)
275
+ else:
276
+ setattr(cls, name, value)
277
+
271
278
  internals["local_fields"] = cls_fields
272
279
 
273
280
  mro = cls.__mro__[:-1] # skip 'object' base class
@@ -352,8 +359,19 @@ class Field:
352
359
  return cls(**argument_dict)
353
360
 
354
361
 
362
+ class GatheredFields:
363
+ __slots__ = ("fields", "modifications")
364
+
365
+ def __init__(self, fields, modifications):
366
+ self.fields = fields
367
+ self.modifications = modifications
368
+
369
+ def __call__(self, cls):
370
+ return self.fields, self.modifications
371
+
372
+
355
373
  # Use the builder to generate __repr__ and __eq__ methods
356
- # and pretend `Field` was a built class all along.
374
+ # for both Field and GatheredFields
357
375
  _field_internal = {
358
376
  "default": Field(default=NOTHING),
359
377
  "default_factory": Field(default=NOTHING),
@@ -361,17 +379,29 @@ _field_internal = {
361
379
  "doc": Field(default=None),
362
380
  }
363
381
 
364
- _field_methods = {repr_desc, eq_desc}
382
+ _gathered_field_internal = {
383
+ "fields": Field(default=NOTHING),
384
+ "modifications": Field(default=NOTHING),
385
+ }
386
+
387
+ _field_methods = {repr_maker, eq_maker}
365
388
  if _UNDER_TESTING:
366
- _field_methods.update({frozen_setattr_desc, frozen_delattr_desc})
389
+ _field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
367
390
 
368
391
  builder(
369
392
  Field,
370
- gatherer=lambda cls_: _field_internal,
393
+ gatherer=GatheredFields(_field_internal, {}),
371
394
  methods=_field_methods,
372
395
  flags={"slotted": True, "kw_only": True},
373
396
  )
374
397
 
398
+ builder(
399
+ GatheredFields,
400
+ gatherer=GatheredFields(_gathered_field_internal, {}),
401
+ methods={repr_maker, eq_maker},
402
+ flags={"slotted": True, "kw_only": False},
403
+ )
404
+
375
405
 
376
406
  # Slot gathering tools
377
407
  # Subclass of dict to be identifiable by isinstance checks
@@ -390,6 +420,14 @@ class SlotFields(dict):
390
420
 
391
421
 
392
422
  def make_slot_gatherer(field_type=Field):
423
+ """
424
+ Create a new annotation gatherer that will work with `Field` instances
425
+ of the creators definition.
426
+
427
+ :param field_type: The `Field` classes to be used when gathering fields
428
+ :return: A slot gatherer that will check for and generate Fields of
429
+ the type field_type.
430
+ """
393
431
  def field_slot_gatherer(cls):
394
432
  """
395
433
  Gather field information for class generation based on __slots__
@@ -405,11 +443,23 @@ def make_slot_gatherer(field_type=Field):
405
443
  "in order to generate a slotclass"
406
444
  )
407
445
 
408
- cls_annotations = cls.__dict__.get("__annotations__", {})
446
+ # Don't want to mutate original annotations so make a copy if it exists
447
+ # Looking at the dict is a Python3.9 or earlier requirement
448
+ cls_annotations = {
449
+ **cls.__dict__.get("__annotations__", {})
450
+ }
451
+
409
452
  cls_fields = {}
410
453
  slot_replacement = {}
411
454
 
412
455
  for k, v in cls_slots.items():
456
+ # Special case __dict__ and __weakref__
457
+ # They should be included in the final `__slots__`
458
+ # But ignored as a value.
459
+ if k in {"__dict__", "__weakref__"}:
460
+ slot_replacement[k] = None
461
+ continue
462
+
413
463
  if isinstance(v, field_type):
414
464
  attrib = v
415
465
  if attrib.type is not NOTHING:
@@ -421,13 +471,15 @@ def make_slot_gatherer(field_type=Field):
421
471
  slot_replacement[k] = attrib.doc
422
472
  cls_fields[k] = attrib
423
473
 
424
- # Replace the SlotAttributes instance with a regular dict
425
- # So that help() works
426
- setattr(cls, "__slots__", slot_replacement)
474
+ # Send the modifications to the builder for what should be changed
475
+ # On the class.
476
+ # In this case, slots with documentation and new annotations.
477
+ modifications = {
478
+ "__slots__": slot_replacement,
479
+ "__annotations__": cls_annotations,
480
+ }
427
481
 
428
- # Update annotations with any types from the slots assignment
429
- setattr(cls, "__annotations__", cls_annotations)
430
- return cls_fields
482
+ return cls_fields, modifications
431
483
 
432
484
  return field_slot_gatherer
433
485
 
@@ -483,6 +535,8 @@ def make_annotation_gatherer(field_type=Field, leave_default_values=True):
483
535
 
484
536
  cls_fields: dict[str, field_type] = {}
485
537
 
538
+ modifications = {}
539
+
486
540
  for k, v in cls_annotations.items():
487
541
  # Ignore ClassVar
488
542
  if is_classvar(v):
@@ -494,20 +548,21 @@ def make_annotation_gatherer(field_type=Field, leave_default_values=True):
494
548
  if isinstance(attrib, field_type):
495
549
  attrib = field_type.from_field(attrib, type=v)
496
550
  if attrib.default is not NOTHING and leave_default_values:
497
- setattr(cls, k, attrib.default)
551
+ modifications[k] = attrib.default
498
552
  else:
499
- delattr(cls, k)
553
+ # NOTHING sentinel indicates a value should be removed
554
+ modifications[k] = NOTHING
500
555
  else:
501
556
  attrib = field_type(default=attrib, type=v)
502
557
  if not leave_default_values:
503
- delattr(cls, k)
558
+ modifications[k] = NOTHING
504
559
 
505
560
  else:
506
561
  attrib = field_type(type=v)
507
562
 
508
563
  cls_fields[k] = attrib
509
564
 
510
- return cls_fields
565
+ return cls_fields, modifications
511
566
 
512
567
  return field_annotation_gatherer
513
568
 
@@ -569,7 +624,7 @@ def annotationclass(cls=None, /, *, methods=default_methods):
569
624
 
570
625
  _field_init_desc = MethodMaker(
571
626
  funcname="__init__",
572
- code_generator=get_init_maker(
627
+ code_generator=get_init_generator(
573
628
  null=_NothingType(),
574
629
  extra_code=["self.validate_field()"],
575
630
  )
@@ -590,11 +645,11 @@ def fieldclass(cls=None, /, *, frozen=False):
590
645
  if not cls:
591
646
  return lambda cls_: fieldclass(cls_, frozen=frozen)
592
647
 
593
- field_methods = {_field_init_desc, repr_desc, eq_desc}
648
+ field_methods = {_field_init_desc, repr_maker, eq_maker}
594
649
 
595
650
  # Always freeze when running tests
596
651
  if frozen or _UNDER_TESTING:
597
- field_methods.update({frozen_setattr_desc, frozen_delattr_desc})
652
+ field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
598
653
 
599
654
  cls = builder(
600
655
  cls,
@@ -26,24 +26,24 @@ class MethodMaker:
26
26
  def __repr__(self) -> str: ...
27
27
  def __get__(self, instance, cls) -> Callable: ...
28
28
 
29
- def get_init_maker(
29
+ def get_init_generator(
30
30
  null: _NothingType = NOTHING,
31
31
  extra_code: None | list[str] = None
32
32
  ) -> Callable[[type], tuple[str, dict[str, typing.Any]]]: ...
33
33
 
34
- def init_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
35
- def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
36
- def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
34
+ def init_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
35
+ def repr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
36
+ def eq_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
37
37
 
38
- def frozen_setattr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
38
+ def frozen_setattr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
39
39
 
40
- def frozen_delattr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
40
+ def frozen_delattr_generator(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
41
41
 
42
- init_desc: MethodMaker
43
- repr_desc: MethodMaker
44
- eq_desc: MethodMaker
45
- frozen_setattr_desc: MethodMaker
46
- frozen_delattr_desc: MethodMaker
42
+ init_maker: MethodMaker
43
+ repr_maker: MethodMaker
44
+ eq_maker: MethodMaker
45
+ frozen_setattr_maker: MethodMaker
46
+ frozen_delattr_maker: MethodMaker
47
47
  default_methods: frozenset[MethodMaker]
48
48
 
49
49
  _T = typing.TypeVar("_T")
@@ -53,7 +53,7 @@ def builder(
53
53
  cls: type[_T],
54
54
  /,
55
55
  *,
56
- gatherer: Callable[[type], dict[str, Field]],
56
+ gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]],
57
57
  methods: frozenset[MethodMaker] | set[MethodMaker],
58
58
  flags: dict[str, bool] | None = None,
59
59
  ) -> type[_T]: ...
@@ -63,7 +63,7 @@ def builder(
63
63
  cls: None = None,
64
64
  /,
65
65
  *,
66
- gatherer: Callable[[type], dict[str, Field]],
66
+ gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]],
67
67
  methods: frozenset[MethodMaker] | set[MethodMaker],
68
68
  flags: dict[str, bool] | None = None,
69
69
  ) -> Callable[[type[_T]], type[_T]]: ...
@@ -92,12 +92,33 @@ class Field:
92
92
  def from_field(cls, fld: Field, /, **kwargs: typing.Any) -> Field: ...
93
93
 
94
94
 
95
+ class GatheredFields:
96
+ __slots__ = ("fields", "modifications")
97
+
98
+ fields: dict[str, Field]
99
+ modifications: dict[str, typing.Any]
100
+
101
+ __classbuilder_internals__: dict
102
+
103
+ def __init__(
104
+ self,
105
+ fields: dict[str, Field],
106
+ modifications: dict[str, typing.Any]
107
+ ) -> None: ...
108
+
109
+ def __repr__(self) -> str: ...
110
+ def __eq__(self, other) -> bool: ...
111
+ def __call__(self, cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
112
+
113
+
95
114
  class SlotFields(dict):
96
115
  ...
97
116
 
98
- def make_slot_gatherer(field_type: type[Field] = Field) -> Callable[[type], dict[str, Field]]: ...
117
+ def make_slot_gatherer(
118
+ field_type: type[Field] = Field
119
+ ) -> Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
99
120
 
100
- def slot_gatherer(cls: type) -> dict[str, Field]:
121
+ def slot_gatherer(cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]:
101
122
  ...
102
123
 
103
124
  def is_classvar(hint: object) -> bool: ...
@@ -105,9 +126,9 @@ def is_classvar(hint: object) -> bool: ...
105
126
  def make_annotation_gatherer(
106
127
  field_type: type[Field] = Field,
107
128
  leave_default_values: bool = True,
108
- ) -> Callable[[type], dict[str, Field]]: ...
129
+ ) -> Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
109
130
 
110
- def annotation_gatherer(cls: type) -> dict[str, Field]: ...
131
+ def annotation_gatherer(cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
111
132
 
112
133
  def check_argument_order(cls: type) -> None: ...
113
134
 
@@ -30,9 +30,9 @@ import sys
30
30
 
31
31
  from . import (
32
32
  INTERNALS_DICT, NOTHING,
33
- Field, MethodMaker, SlotFields,
33
+ Field, MethodMaker, SlotFields, GatheredFields,
34
34
  builder, fieldclass, get_flags, get_fields, make_slot_gatherer,
35
- frozen_setattr_desc, frozen_delattr_desc, is_classvar,
35
+ frozen_setattr_maker, frozen_delattr_maker, is_classvar,
36
36
  )
37
37
 
38
38
  PREFAB_FIELDS = "PREFAB_FIELDS"
@@ -344,13 +344,13 @@ def get_asdict_maker():
344
344
  return MethodMaker("as_dict", as_dict_gen)
345
345
 
346
346
 
347
- init_desc = get_init_maker()
348
- prefab_init_desc = get_init_maker(init_name=PREFAB_INIT_FUNC)
349
- repr_desc = get_repr_maker()
350
- recursive_repr_desc = get_repr_maker(recursion_safe=True)
351
- eq_desc = get_eq_maker()
352
- iter_desc = get_iter_maker()
353
- asdict_desc = get_asdict_maker()
347
+ init_maker = get_init_maker()
348
+ 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()
352
+ iter_maker = get_iter_maker()
353
+ asdict_maker = get_asdict_maker()
354
354
 
355
355
 
356
356
  # Updated field with additional attributes
@@ -441,6 +441,8 @@ def attribute_gatherer(cls):
441
441
 
442
442
  cls_attribute_names = cls_attributes.keys()
443
443
 
444
+ cls_modifications = {}
445
+
444
446
  if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
445
447
  # replace the classes' attributes dict with one with the correct
446
448
  # order from the annotations.
@@ -483,7 +485,7 @@ def attribute_gatherer(cls):
483
485
 
484
486
  # Clear the attribute from the class after it has been used
485
487
  # in the definition.
486
- delattr(cls, name)
488
+ cls_modifications[name] = NOTHING
487
489
  else:
488
490
  attrib = attribute(**extras)
489
491
 
@@ -493,14 +495,14 @@ def attribute_gatherer(cls):
493
495
  else:
494
496
  for name in cls_attributes.keys():
495
497
  attrib = cls_attributes[name]
496
- delattr(cls, name) # clear attrib from class
498
+ cls_modifications[name] = NOTHING
497
499
 
498
500
  # Some items can still be annotated.
499
501
  if name in cls_annotations:
500
502
  new_attrib = Attribute.from_field(attrib, type=cls_annotations[name])
501
503
  cls_attributes[name] = new_attrib
502
504
 
503
- return cls_attributes
505
+ return cls_attributes, cls_modifications
504
506
 
505
507
 
506
508
  # Class Builders
@@ -517,6 +519,7 @@ def _make_prefab(
517
519
  frozen=False,
518
520
  dict_method=False,
519
521
  recursive_repr=False,
522
+ gathered_fields=None,
520
523
  ):
521
524
  """
522
525
  Generate boilerplate code for dunder methods in a class.
@@ -533,6 +536,7 @@ def _make_prefab(
533
536
  such as lists)
534
537
  :param dict_method: Include an as_dict method for faster dictionary creation
535
538
  :param recursive_repr: Safely handle repr in case of recursion
539
+ :param gathered_fields: Pre-gathered fields callable, to skip re-collecting attributes
536
540
  :return: class with __ methods defined
537
541
  """
538
542
  cls_dict = cls.__dict__
@@ -544,34 +548,38 @@ def _make_prefab(
544
548
  )
545
549
 
546
550
  slots = cls_dict.get("__slots__")
547
- if isinstance(slots, SlotFields):
548
- gatherer = slot_prefab_gatherer
549
- slotted = True
551
+ if gathered_fields is None:
552
+ if isinstance(slots, SlotFields):
553
+ gatherer = slot_prefab_gatherer
554
+ slotted = True
555
+ else:
556
+ gatherer = attribute_gatherer
557
+ slotted = False
550
558
  else:
551
- gatherer = attribute_gatherer
552
- slotted = False
559
+ gatherer = gathered_fields
560
+ slotted = False if slots is None else True
553
561
 
554
562
  methods = set()
555
563
 
556
564
  if init and "__init__" not in cls_dict:
557
- methods.add(init_desc)
565
+ methods.add(init_maker)
558
566
  else:
559
- methods.add(prefab_init_desc)
567
+ methods.add(prefab_init_maker)
560
568
 
561
569
  if repr and "__repr__" not in cls_dict:
562
570
  if recursive_repr:
563
- methods.add(recursive_repr_desc)
571
+ methods.add(recursive_repr_maker)
564
572
  else:
565
- methods.add(repr_desc)
573
+ methods.add(repr_maker)
566
574
  if eq and "__eq__" not in cls_dict:
567
- methods.add(eq_desc)
575
+ methods.add(eq_maker)
568
576
  if iter and "__iter__" not in cls_dict:
569
- methods.add(iter_desc)
577
+ methods.add(iter_maker)
570
578
  if frozen:
571
- methods.add(frozen_setattr_desc)
572
- methods.add(frozen_delattr_desc)
579
+ methods.add(frozen_setattr_maker)
580
+ methods.add(frozen_delattr_maker)
573
581
  if dict_method:
574
- methods.add(asdict_desc)
582
+ methods.add(asdict_maker)
575
583
 
576
584
  flags = {
577
585
  "kw_only": kw_only,
@@ -768,6 +776,7 @@ def build_prefab(
768
776
  frozen=False,
769
777
  dict_method=False,
770
778
  recursive_repr=False,
779
+ slots=False,
771
780
  ):
772
781
  """
773
782
  Dynamically construct a (dynamic) prefab.
@@ -788,12 +797,35 @@ def build_prefab(
788
797
  (This does not prevent the modification of mutable attributes such as lists)
789
798
  :param dict_method: Include an as_dict method for faster dictionary creation
790
799
  :param recursive_repr: Safely handle repr in case of recursion
800
+ :param slots: Make the resulting class slotted
791
801
  :return: class with __ methods defined
792
802
  """
793
- class_dict = {} if class_dict is None else class_dict
794
- cls = type(class_name, bases, class_dict)
803
+ class_dict = {} if class_dict is None else class_dict.copy()
804
+
805
+ class_annotations = {}
806
+ class_slots = {}
807
+ fields = {}
808
+
795
809
  for name, attrib in attributes:
796
- setattr(cls, name, attrib)
810
+ if isinstance(attrib, Attribute):
811
+ fields[name] = attrib
812
+ elif isinstance(attrib, Field):
813
+ fields[name] = Attribute.from_field(attrib)
814
+ else:
815
+ fields[name] = Attribute(default=attrib)
816
+
817
+ if attrib.type is not NOTHING:
818
+ class_annotations[name] = attrib.type
819
+
820
+ class_slots[name] = attrib.doc
821
+
822
+ if slots:
823
+ class_dict["__slots__"] = class_slots
824
+
825
+ class_dict["__annotations__"] = class_annotations
826
+ cls = type(class_name, bases, class_dict)
827
+
828
+ gathered_fields = GatheredFields(fields, {})
797
829
 
798
830
  cls = _make_prefab(
799
831
  cls,
@@ -806,6 +838,7 @@ def build_prefab(
806
838
  frozen=frozen,
807
839
  dict_method=dict_method,
808
840
  recursive_repr=recursive_repr,
841
+ gathered_fields=gathered_fields,
809
842
  )
810
843
 
811
844
  return cls
@@ -847,17 +880,17 @@ def as_dict(o):
847
880
  :param o: instance of a prefab class
848
881
  :return: dictionary of {k: v} from fields
849
882
  """
883
+ cls = type(o)
884
+ if not hasattr(cls, PREFAB_FIELDS):
885
+ raise TypeError(f"{o!r} should be a prefab instance, not {cls}")
886
+
850
887
  # Attempt to use the generated method if available
851
888
  try:
852
889
  return o.as_dict()
853
890
  except AttributeError:
854
891
  pass
855
892
 
856
- cls = type(o)
857
- try:
858
- flds = get_attributes(cls)
859
- except AttributeError:
860
- raise TypeError(f"inst should be a prefab instance, not {cls}")
893
+ flds = get_attributes(cls)
861
894
 
862
895
  return {
863
896
  name: getattr(o, name)
@@ -6,7 +6,7 @@ from collections.abc import Callable
6
6
  from . import (
7
7
  INTERNALS_DICT, NOTHING,
8
8
  Field, MethodMaker, SlotFields as SlotFields,
9
- builder, fieldclass, get_flags, get_fields, slot_gatherer
9
+ builder, fieldclass, get_flags, get_fields, make_slot_gatherer
10
10
  )
11
11
 
12
12
  # noinspection PyUnresolvedReferences
@@ -39,13 +39,13 @@ def get_iter_maker() -> MethodMaker: ...
39
39
  def get_asdict_maker() -> MethodMaker: ...
40
40
 
41
41
 
42
- init_desc: MethodMaker
43
- prefab_init_desc: MethodMaker
44
- repr_desc: MethodMaker
45
- recursive_repr_desc: MethodMaker
46
- eq_desc: MethodMaker
47
- iter_desc: MethodMaker
48
- asdict_desc: MethodMaker
42
+ init_maker: MethodMaker
43
+ prefab_init_maker: MethodMaker
44
+ repr_maker: MethodMaker
45
+ recursive_repr_maker: MethodMaker
46
+ eq_maker: MethodMaker
47
+ iter_maker: MethodMaker
48
+ asdict_maker: MethodMaker
49
49
 
50
50
  class Attribute(Field):
51
51
  __slots__: dict
@@ -93,9 +93,9 @@ def attribute(
93
93
  exclude_field: bool = False,
94
94
  ) -> Attribute: ...
95
95
 
96
- def slot_prefab_gatherer(cls: type) -> dict[str, Attribute]: ...
96
+ def slot_prefab_gatherer(cls: type) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
97
97
 
98
- def attribute_gatherer(cls: type) -> dict[str, Attribute]: ...
98
+ def attribute_gatherer(cls: type) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
99
99
 
100
100
  def _make_prefab(
101
101
  cls: type,
@@ -109,6 +109,7 @@ def _make_prefab(
109
109
  frozen: bool = False,
110
110
  dict_method: bool = False,
111
111
  recursive_repr: bool = False,
112
+ gathered_fields: Callable[[type], tuple[dict[str, Attribute], dict[str, typing.Any]]] | None = None,
112
113
  ) -> type: ...
113
114
 
114
115
  _T = typing.TypeVar("_T")
@@ -146,6 +147,7 @@ def build_prefab(
146
147
  frozen: bool = False,
147
148
  dict_method: bool = False,
148
149
  recursive_repr: bool = False,
150
+ slots: bool = False,
149
151
  ) -> type: ...
150
152
 
151
153
  def is_prefab(o: typing.Any) -> bool: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -69,11 +69,12 @@ Install from PyPI with:
69
69
  In order to create a class decorator using `ducktools.classbuilder` there are
70
70
  a few things you need to prepare.
71
71
 
72
- 1. A field gathering function to analyse the class and collect valid `Field`s.
72
+ 1. A field gathering function to analyse the class and collect valid `Field`s and provide
73
+ any modifications that need to be applied to the class attributes.
73
74
  * An example `slot_gatherer` is included.
74
75
  2. Code generators that can make use of the gathered `Field`s to create magic method
75
- source code.
76
- * Example `init_maker`, `repr_maker` and `eq_maker` generators are included.
76
+ source code. To be made into descriptors by `MethodMaker`.
77
+ * Example `init_generator`, `repr_generator` and `eq_generator` generators are included.
77
78
  3. A function that calls the `builder` function to apply both of these steps.
78
79
 
79
80
  A field gathering function needs to take the original class as an argument and
@@ -91,25 +92,26 @@ class[^1] where keyword arguments define the names and values for the fields.
91
92
 
92
93
  Code generator functions need to be converted to descriptors before being used.
93
94
  This is done using the provided `MethodMaker` descriptor class.
94
- ex: `init_desc = MethodMaker("__init__", init_maker)`
95
+ ex: `init_maker = MethodMaker("__init__", init_generator)`.
95
96
 
96
97
  These parts can then be used to make a basic class boilerplate generator by
97
98
  providing them to the `builder` function.
98
99
 
99
100
  ```python
100
101
  from ducktools.classbuilder import (
101
- builder,
102
- slot_gatherer,
103
- init_maker, eq_maker, repr_maker,
102
+ builder,
103
+ slot_gatherer,
104
+ init_generator, eq_generator, repr_generator,
104
105
  MethodMaker,
105
106
  )
106
107
 
107
- init_desc = MethodMaker("__init__", init_maker)
108
- repr_desc = MethodMaker("__repr__", repr_maker)
109
- eq_desc = MethodMaker("__eq__", eq_maker)
108
+ init_maker = MethodMaker("__init__", init_generator)
109
+ repr_maker = MethodMaker("__repr__", repr_generator)
110
+ eq_maker = MethodMaker("__eq__", eq_generator)
111
+
110
112
 
111
113
  def slotclass(cls):
112
- return builder(cls, gatherer=slot_gatherer, methods={init_desc, repr_desc, eq_desc})
114
+ return builder(cls, gatherer=slot_gatherer, methods={init_maker, repr_maker, eq_maker})
113
115
  ```
114
116
 
115
117
  ## Slot Class Usage ##
@@ -238,9 +240,8 @@ It will copy values provided as the `type` to `Field` into the
238
240
  Values provided to `doc` will be placed in the final `__slots__`
239
241
  field so they are present on the class if `help(...)` is called.
240
242
 
241
- A fairly basic `annotations_gatherer` and `annotationclass` are included
242
- in `extras.py` which can be used to generate classbuilders that rely on
243
- annotations.
243
+ A fairly basic `annotations_gatherer` and `annotationclass` are also included
244
+ and can be used to generate classbuilders that rely on annotations.
244
245
 
245
246
  If you want something with more features you can look at the `prefab.py`
246
247
  implementation which provides a 'prebuilt' implementation.
@@ -0,0 +1,10 @@
1
+ ducktools/classbuilder/__init__.py,sha256=rl5oc0azae-32EjeCTzhM1DR5JNR-AgOriyH5zkf4WI,20856
2
+ ducktools/classbuilder/__init__.pyi,sha256=5gnHhSVMP5yArA-_cuvIeLnLAlmY_EmPMfVY9dRi0CA,4784
3
+ ducktools/classbuilder/prefab.py,sha256=K9_bKh24OiGJ40BbJWVfwDObArMUxi2JRbB5ZN-t--M,28984
4
+ ducktools/classbuilder/prefab.pyi,sha256=sNladRJM3lcZo8zQWvWCIInBx0wUD7PT6fhdE89OpG0,3960
5
+ ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
6
+ ducktools_classbuilder-0.5.1.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
7
+ ducktools_classbuilder-0.5.1.dist-info/METADATA,sha256=B0KO5vvdZOUZ4o28GA4zlgXuIkA9j4SuplzFByvRhjs,10143
8
+ ducktools_classbuilder-0.5.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
+ ducktools_classbuilder-0.5.1.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
10
+ ducktools_classbuilder-0.5.1.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- ducktools/classbuilder/__init__.py,sha256=7OkqvfHLSSJylkAeiErgZXB9E-lDUK-rvbu3CRWFSdc,19113
2
- ducktools/classbuilder/__init__.pyi,sha256=64cp1-zpOh2bUVlwcoTTdaampFINIhdFbFDcleE3N0g,4091
3
- ducktools/classbuilder/prefab.py,sha256=9K_02AJ3-gLR0Cb0RxKwd2n_cwVtE2mf1TE2n_VNp4o,27947
4
- ducktools/classbuilder/prefab.pyi,sha256=5MiPuuT4hc-Er4xFfrEkBqz-dCjULYJfTM_c0upa0Dc,3758
5
- ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
6
- ducktools_classbuilder-0.4.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
7
- ducktools_classbuilder-0.4.0.dist-info/METADATA,sha256=teRrGNNsVoQJ1jLnzctDVFREtSDq9MVCS9GNZr1S01E,9982
8
- ducktools_classbuilder-0.4.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
- ducktools_classbuilder-0.4.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
10
- ducktools_classbuilder-0.4.0.dist-info/RECORD,,