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

@@ -19,7 +19,7 @@
19
19
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
- __version__ = "v0.1.0"
22
+ __version__ = "v0.2.0"
23
23
 
24
24
  # Change this name if you make heavy modifications
25
25
  INTERNALS_DICT = "__classbuilder_internals__"
@@ -34,6 +34,9 @@ def get_internals(cls):
34
34
  and 'local_fields' attributes this will always
35
35
  evaluate as 'truthy' if this is a generated class.
36
36
 
37
+ Generally you should use the helper get_flags and
38
+ get_fields methods.
39
+
37
40
  Usage:
38
41
  if internals := get_internals(cls):
39
42
  ...
@@ -44,15 +47,28 @@ def get_internals(cls):
44
47
  return getattr(cls, INTERNALS_DICT, None)
45
48
 
46
49
 
47
- def get_fields(cls):
50
+ def get_fields(cls, *, local=False):
48
51
  """
49
52
  Utility function to gather the fields dictionary
50
53
  from the class internals.
51
54
 
52
55
  :param cls: generated class
56
+ :param local: get only fields that were not inherited
53
57
  :return: dictionary of keys and Field attribute info
54
58
  """
55
- return getattr(cls, INTERNALS_DICT)["fields"]
59
+ key = "local_fields" if local else "fields"
60
+ return getattr(cls, INTERNALS_DICT)[key]
61
+
62
+
63
+ def get_flags(cls):
64
+ """
65
+ Utility function to gather the flags dictionary
66
+ from the class internals.
67
+
68
+ :param cls: generated class
69
+ :return: dictionary of keys and flag values
70
+ """
71
+ return getattr(cls, INTERNALS_DICT)["flags"]
56
72
 
57
73
 
58
74
  def get_inst_fields(inst):
@@ -106,14 +122,15 @@ class MethodMaker:
106
122
  return method.__get__(instance, cls)
107
123
 
108
124
 
109
- def init_maker(cls, *, null=NOTHING, kw_only=False):
125
+ def init_maker(cls, *, null=NOTHING):
110
126
  fields = get_fields(cls)
127
+ flags = get_flags(cls)
111
128
 
112
129
  arglist = []
113
130
  assignments = []
114
131
  globs = {}
115
132
 
116
- if kw_only:
133
+ if flags.get("kw_only", False):
117
134
  arglist.append("*")
118
135
 
119
136
  for k, v in fields.items():
@@ -180,7 +197,7 @@ eq_desc = MethodMaker("__eq__", eq_maker)
180
197
  default_methods = frozenset({init_desc, repr_desc, eq_desc})
181
198
 
182
199
 
183
- def builder(cls=None, /, *, gatherer, methods):
200
+ def builder(cls=None, /, *, gatherer, methods, flags=None):
184
201
  """
185
202
  The main builder for class generation
186
203
 
@@ -189,6 +206,8 @@ def builder(cls=None, /, *, gatherer, methods):
189
206
  :type gatherer: Callable[[type], dict[str, Field]]
190
207
  :param methods: MethodMakers to add to the class
191
208
  :type methods: set[MethodMaker]
209
+ :param flags: additional flags to store in the internals dictionary
210
+ for use by method generators.
192
211
  :return: The modified class (the class itself is modified, but this is expected).
193
212
  """
194
213
  # Handle `None` to make wrapping with a decorator easier.
@@ -197,6 +216,7 @@ def builder(cls=None, /, *, gatherer, methods):
197
216
  cls_,
198
217
  gatherer=gatherer,
199
218
  methods=methods,
219
+ flags=flags,
200
220
  )
201
221
 
202
222
  internals = {}
@@ -212,11 +232,12 @@ def builder(cls=None, /, *, gatherer, methods):
212
232
  fields = {}
213
233
  for c in reversed(mro):
214
234
  try:
215
- fields.update(get_internals(c)["local_fields"])
235
+ fields.update(get_fields(c, local=True))
216
236
  except AttributeError:
217
237
  pass
218
238
 
219
239
  internals["fields"] = fields
240
+ internals["flags"] = flags if flags is not None else {}
220
241
 
221
242
  # Assign all of the method generators
222
243
  for method in methods:
@@ -296,7 +317,8 @@ _field_internal = {
296
317
  builder(
297
318
  Field,
298
319
  gatherer=lambda cls_: _field_internal,
299
- methods=frozenset({repr_desc, eq_desc})
320
+ methods=frozenset({repr_desc, eq_desc}),
321
+ flags={"slotted": True, "kw_only": True},
300
322
  )
301
323
 
302
324
 
@@ -368,7 +390,7 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
368
390
  if not cls:
369
391
  return lambda cls_: slotclass(cls_, methods=methods, syntax_check=syntax_check)
370
392
 
371
- cls = builder(cls, gatherer=slot_gatherer, methods=methods)
393
+ cls = builder(cls, gatherer=slot_gatherer, methods=methods, flags={"slotted": True})
372
394
 
373
395
  if syntax_check:
374
396
  fields = get_fields(cls)
@@ -398,7 +420,7 @@ def fieldclass(cls):
398
420
  # Fields need a way to call their validate method
399
421
  # So append it to the code from __init__.
400
422
  def field_init_func(cls_):
401
- code, globs = init_maker(cls_, null=field_nothing, kw_only=True)
423
+ code, globs = init_maker(cls_, null=field_nothing)
402
424
  code += " self.validate_field()\n"
403
425
  return code, globs
404
426
 
@@ -412,7 +434,8 @@ def fieldclass(cls):
412
434
  cls = builder(
413
435
  cls,
414
436
  gatherer=slot_gatherer,
415
- methods=field_methods
437
+ methods=field_methods,
438
+ flags={"slotted": True, "kw_only": True}
416
439
  )
417
440
 
418
- return cls
441
+ return cls
@@ -1,12 +1,16 @@
1
1
  import typing
2
2
  from collections.abc import Callable
3
3
 
4
+ _py_type = type # Alias for type where it is used as a name
5
+
4
6
  __version__: str
5
7
  INTERNALS_DICT: str
6
8
 
7
9
  def get_internals(cls) -> dict[str, typing.Any] | None: ...
8
10
 
9
- def get_fields(cls: type) -> dict[str, Field]: ...
11
+ def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...
12
+
13
+ def get_flags(cls:type) -> dict[str, bool]: ...
10
14
 
11
15
  def get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
12
16
 
@@ -28,7 +32,6 @@ def init_maker(
28
32
  cls: type,
29
33
  *,
30
34
  null: _NothingType = NOTHING,
31
- kw_only: bool = False
32
35
  ) -> tuple[str, dict[str, typing.Any]]: ...
33
36
  def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
34
37
  def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
@@ -38,14 +41,17 @@ repr_desc: MethodMaker
38
41
  eq_desc: MethodMaker
39
42
  default_methods: frozenset[MethodMaker]
40
43
 
44
+ _T = typing.TypeVar("_T")
45
+
41
46
  @typing.overload
42
47
  def builder(
43
- cls: type,
48
+ cls: type[_T],
44
49
  /,
45
50
  *,
46
51
  gatherer: Callable[[type], dict[str, Field]],
47
- methods: frozenset[MethodMaker] | set[MethodMaker]
48
- ) -> typing.Any: ...
52
+ methods: frozenset[MethodMaker] | set[MethodMaker],
53
+ flags: dict[str, bool] | None = None,
54
+ ) -> type[_T]: ...
49
55
 
50
56
  @typing.overload
51
57
  def builder(
@@ -53,36 +59,32 @@ def builder(
53
59
  /,
54
60
  *,
55
61
  gatherer: Callable[[type], dict[str, Field]],
56
- methods: frozenset[MethodMaker] | set[MethodMaker]
57
- ) -> Callable[[type], type]: ...
62
+ methods: frozenset[MethodMaker] | set[MethodMaker],
63
+ flags: dict[str, bool] | None = None,
64
+ ) -> Callable[[type[_T]], type[_T]]: ...
58
65
 
59
66
 
60
- _Self = typing.TypeVar("_Self", bound="Field")
61
-
62
67
  class Field:
63
68
  default: _NothingType | typing.Any
64
69
  default_factory: _NothingType | typing.Any
65
- type: _NothingType | type
70
+ type: _NothingType | _py_type
66
71
  doc: None | str
67
72
 
73
+ __classbuilder_internals__: dict
74
+
68
75
  def __init__(
69
76
  self,
70
77
  *,
71
78
  default: _NothingType | typing.Any = NOTHING,
72
79
  default_factory: _NothingType | typing.Any = NOTHING,
73
- type: _NothingType | type = NOTHING,
80
+ type: _NothingType | _py_type = NOTHING,
74
81
  doc: None | str = None,
75
82
  ) -> None: ...
76
- @property
77
- def _inherited_slots(self) -> list[str]: ...
78
83
  def __repr__(self) -> str: ...
79
- @typing.overload
80
- def __eq__(self, other: _Self) -> bool: ...
81
- @typing.overload
82
- def __eq__(self, other: object) -> NotImplemented: ...
84
+ def __eq__(self, other: Field | object) -> bool: ...
83
85
  def validate_field(self) -> None: ...
84
86
  @classmethod
85
- def from_field(cls, fld: Field, **kwargs: typing.Any) -> _Self: ...
87
+ def from_field(cls, fld: Field, /, **kwargs: typing.Any) -> Field: ...
86
88
 
87
89
 
88
90
  class SlotFields(dict):
@@ -93,19 +95,20 @@ def slot_gatherer(cls: type) -> dict[str, Field]:
93
95
 
94
96
  @typing.overload
95
97
  def slotclass(
96
- cls: type,
98
+ cls: type[_T],
97
99
  /,
98
100
  *,
99
101
  methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
100
102
  syntax_check: bool = True
101
- ) -> typing.Any: ...
103
+ ) -> type[_T]: ...
102
104
 
105
+ @typing.overload
103
106
  def slotclass(
104
107
  cls: None = None,
105
108
  /,
106
109
  *,
107
110
  methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
108
111
  syntax_check: bool = True
109
- ) -> Callable[[type], type]: ...
112
+ ) -> Callable[[type[_T]], type[_T]]: ...
110
113
 
111
- def fieldclass(cls: type) -> typing.Any: ...
114
+ def fieldclass(cls: type[_T]) -> type[_T]: ...
@@ -31,7 +31,7 @@ import sys
31
31
  from . import (
32
32
  INTERNALS_DICT, NOTHING,
33
33
  Field, MethodMaker, SlotFields,
34
- builder, fieldclass, get_internals, slot_gatherer
34
+ builder, fieldclass, get_flags, get_fields, slot_gatherer
35
35
  )
36
36
 
37
37
  PREFAB_FIELDS = "PREFAB_FIELDS"
@@ -84,10 +84,11 @@ def get_attributes(cls):
84
84
  def get_init_maker(*, init_name="__init__"):
85
85
  def __init__(cls: "type") -> "tuple[str, dict]":
86
86
  globs = {}
87
- internals = get_internals(cls)
88
87
  # Get the internals dictionary and prepare attributes
89
- attributes = internals["fields"]
90
- kw_only = internals["kw_only"]
88
+ attributes = get_attributes(cls)
89
+ flags = get_flags(cls)
90
+
91
+ kw_only = flags.get("kw_only", False)
91
92
 
92
93
  # Handle pre/post init first - post_init can change types for __init__
93
94
  # Get pre and post init arguments
@@ -319,12 +320,19 @@ def get_eq_maker():
319
320
 
320
321
  def get_iter_maker():
321
322
  def __iter__(cls: "type") -> "tuple[str, dict]":
322
- field_names = get_attributes(cls).keys()
323
+ fields = get_attributes(cls)
323
324
 
324
- if field_names:
325
- values = "\n".join(f" yield self.{name} " for name in field_names)
326
- else:
325
+ valid_fields = (
326
+ name for name, attrib in fields.items()
327
+ if attrib.iter and not attrib.exclude_field
328
+ )
329
+
330
+ values = "\n".join(f" yield self.{name}" for name in valid_fields)
331
+
332
+ # if values is an empty string
333
+ if not values:
327
334
  values = " yield from ()"
335
+
328
336
  code = f"def __iter__(self):\n{values}"
329
337
  globs = {}
330
338
  return code, globs
@@ -335,14 +343,16 @@ def get_iter_maker():
335
343
  def get_frozen_setattr_maker():
336
344
  def __setattr__(cls: "type") -> "tuple[str, dict]":
337
345
  globs = {}
338
- internals = get_internals(cls)
339
- field_names = internals["fields"].keys()
346
+ attributes = get_attributes(cls)
347
+ flags = get_flags(cls)
340
348
 
341
349
  # Make the fields set literal
342
- fields_delimited = ", ".join(f"{field!r}" for field in field_names)
350
+ fields_delimited = ", ".join(f"{field!r}" for field in attributes)
343
351
  field_set = f"{{ {fields_delimited} }}"
344
352
 
345
- if internals["slotted"]:
353
+ # Better to be safe and use the method that works in both cases
354
+ # if somehow slotted has not been set.
355
+ if flags.get("slotted", True):
346
356
  globs["__prefab_setattr_func"] = object.__setattr__
347
357
  setattr_method = "__prefab_setattr_func(self, name, value)"
348
358
  else:
@@ -366,6 +376,7 @@ def get_frozen_setattr_maker():
366
376
 
367
377
 
368
378
  def get_frozen_delattr_maker():
379
+ # noinspection PyUnusedLocal
369
380
  def __delattr__(cls: "type") -> "tuple[str, dict]":
370
381
  body = (
371
382
  ' raise TypeError(\n'
@@ -415,6 +426,7 @@ class Attribute(Field):
415
426
  init=True,
416
427
  repr=True,
417
428
  compare=True,
429
+ iter=True,
418
430
  kw_only=False,
419
431
  in_dict=True,
420
432
  exclude_field=False,
@@ -436,6 +448,7 @@ def attribute(
436
448
  init=True,
437
449
  repr=True,
438
450
  compare=True,
451
+ iter=True,
439
452
  kw_only=False,
440
453
  in_dict=True,
441
454
  exclude_field=False,
@@ -443,8 +456,7 @@ def attribute(
443
456
  type=NOTHING,
444
457
  ):
445
458
  """
446
- Additional definition for how to generate standard methods
447
- for an instance attribute.
459
+ Get an object to define a prefab Attribute
448
460
 
449
461
  :param default: Default value for this attribute
450
462
  :param default_factory: 0 argument callable to give a default value
@@ -452,6 +464,7 @@ def attribute(
452
464
  :param init: Include this attribute in the __init__ parameters
453
465
  :param repr: Include this attribute in the class __repr__
454
466
  :param compare: Include this attribute in the class __eq__
467
+ :param iter: Include this attribute in the class __iter__ if generated
455
468
  :param kw_only: Make this argument keyword only in init
456
469
  :param in_dict: Include this attribute in methods that serialise to dict
457
470
  :param exclude_field: Exclude this field from all magic method generation
@@ -469,6 +482,7 @@ def attribute(
469
482
  init=init,
470
483
  repr=repr,
471
484
  compare=compare,
485
+ iter=iter,
472
486
  kw_only=kw_only,
473
487
  in_dict=in_dict,
474
488
  exclude_field=exclude_field,
@@ -477,6 +491,14 @@ def attribute(
477
491
  )
478
492
 
479
493
 
494
+ def slot_prefab_gatherer(cls):
495
+ # For prefabs it's easier if everything is an attribute
496
+ return {
497
+ name: Attribute.from_field(fld)
498
+ for name, fld in slot_gatherer(cls).items()
499
+ }
500
+
501
+
480
502
  # Gatherer for classes built on attributes or annotations
481
503
  def attribute_gatherer(cls):
482
504
  cls_annotations = cls.__dict__.get("__annotations__", {})
@@ -588,7 +610,7 @@ def _make_prefab(
588
610
 
589
611
  slots = cls_dict.get("__slots__")
590
612
  if isinstance(slots, SlotFields):
591
- gatherer = slot_gatherer
613
+ gatherer = slot_prefab_gatherer
592
614
  slotted = True
593
615
  else:
594
616
  gatherer = attribute_gatherer
@@ -616,18 +638,20 @@ def _make_prefab(
616
638
  if dict_method:
617
639
  methods.add(asdict_desc)
618
640
 
641
+ flags = {
642
+ "kw_only": kw_only,
643
+ "slotted": slotted,
644
+ }
645
+
619
646
  cls = builder(
620
647
  cls,
621
648
  gatherer=gatherer,
622
649
  methods=methods,
650
+ flags=flags,
623
651
  )
624
652
 
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"]
653
+ # Get fields now the class has been built
654
+ fields = get_fields(cls)
631
655
 
632
656
  # Check pre_init and post_init functions if they exist
633
657
  try:
@@ -699,14 +723,12 @@ def _make_prefab(
699
723
  if not isinstance(attrib, Attribute):
700
724
  attrib = Attribute.from_field(attrib)
701
725
  fields[name] = attrib
702
- if name in local_fields:
703
- local_fields[name] = attrib
704
726
 
705
727
  # Excluded fields *MUST* be forwarded to post_init
706
728
  if attrib.exclude_field:
707
729
  if name not in post_init_args:
708
730
  raise PrefabError(
709
- f"{name} is an excluded attribute but is not passed to post_init"
731
+ f"{name!r} is an excluded attribute but is not passed to post_init"
710
732
  )
711
733
  else:
712
734
  valid_args.append(name)
@@ -1,4 +1,6 @@
1
1
  import typing
2
+ from typing_extensions import dataclass_transform
3
+
2
4
  from collections.abc import Callable
3
5
 
4
6
  from . import (
@@ -7,6 +9,9 @@ from . import (
7
9
  builder, fieldclass, get_internals, slot_gatherer
8
10
  )
9
11
 
12
+ # noinspection PyUnresolvedReferences
13
+ from . import _NothingType
14
+
10
15
  PREFAB_FIELDS: str
11
16
  PREFAB_INIT_FUNC: str
12
17
  PRE_INIT_FUNC: str
@@ -56,6 +61,7 @@ class Attribute(Field):
56
61
  init: bool
57
62
  repr: bool
58
63
  compare: bool
64
+ iter: bool
59
65
  kw_only: bool
60
66
  in_dict: bool
61
67
  exclude_field: bool
@@ -63,39 +69,40 @@ class Attribute(Field):
63
69
  def __init__(
64
70
  self,
65
71
  *,
66
- default: typing.Any | NOTHING =NOTHING,
67
- default_factory: typing.Any | NOTHING = NOTHING,
68
- type: type | NOTHING = NOTHING,
72
+ default: typing.Any | _NothingType = NOTHING,
73
+ default_factory: typing.Any | _NothingType = NOTHING,
74
+ type: type | _NothingType = NOTHING,
69
75
  doc: str | None = None,
70
76
  init: bool = True,
71
77
  repr: bool = True,
72
78
  compare: bool = True,
79
+ iter: bool = True,
73
80
  kw_only: bool = False,
74
81
  in_dict: bool = True,
75
82
  exclude_field: bool = False,
76
83
  ) -> None: ...
77
84
 
78
85
  def __repr__(self) -> str: ...
79
- @typing.overload
80
- def __eq__(self, other: Attribute) -> bool: ...
81
- def __eq__(self, other: object) -> NotImplemented: ...
82
-
86
+ def __eq__(self, other: Attribute | object) -> bool: ...
83
87
  def validate_field(self) -> None: ...
84
88
 
85
89
  def attribute(
86
90
  *,
87
- default: typing.Any | NOTHING = NOTHING,
88
- default_factory: typing.Any | NOTHING = NOTHING,
89
- type: type | NOTHING = NOTHING,
91
+ default: typing.Any | _NothingType = NOTHING,
92
+ default_factory: typing.Any | _NothingType = NOTHING,
93
+ type: type | _NothingType = NOTHING,
90
94
  doc: str | None = None,
91
95
  init: bool = True,
92
96
  repr: bool = True,
93
97
  compare: bool = True,
98
+ iter: bool = True,
94
99
  kw_only: bool = False,
95
100
  in_dict: bool = True,
96
101
  exclude_field: bool = False,
97
102
  ) -> Attribute: ...
98
103
 
104
+ def slot_prefab_gatherer(cls: type) -> dict[str, Attribute]: ...
105
+
99
106
  def attribute_gatherer(cls: type) -> dict[str, Attribute]: ...
100
107
 
101
108
  def _make_prefab(
@@ -112,9 +119,14 @@ def _make_prefab(
112
119
  recursive_repr: bool = False,
113
120
  ) -> type: ...
114
121
 
115
- @typing.dataclass_transform
122
+ _T = typing.TypeVar("_T")
123
+
124
+
125
+ # For some reason PyCharm can't see 'attribute'?!?
126
+ # noinspection PyUnresolvedReferences
127
+ @dataclass_transform(field_specifiers=(Attribute, attribute))
116
128
  def prefab(
117
- cls: type | None = None,
129
+ cls: type[_T] | None = None,
118
130
  *,
119
131
  init: bool = True,
120
132
  repr: bool = True,
@@ -125,7 +137,7 @@ def prefab(
125
137
  frozen: bool = False,
126
138
  dict_method: bool = False,
127
139
  recursive_repr: bool = False,
128
- ) -> type | Callable[[type], type]: ...
140
+ ) -> type[_T] | Callable[[type[_T]], type[_T]]: ...
129
141
 
130
142
  def build_prefab(
131
143
  class_name: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -44,6 +44,7 @@ Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
44
44
  Provides-Extra: testing
45
45
  Requires-Dist: pytest ; extra == 'testing'
46
46
  Requires-Dist: pytest-cov ; extra == 'testing'
47
+ Requires-Dist: mypy ; extra == 'testing'
47
48
 
48
49
  # Ducktools: Class Builder #
49
50
 
@@ -53,19 +54,74 @@ of writing... functions... that will bring back the **joy** of writing classes.
53
54
  Maybe.
54
55
 
55
56
  While `attrs` and `dataclasses` are class boilerplate generators,
56
- `ducktools.classbuilder` is intended to be a dataclasses-like generator.
57
+ `ducktools.classbuilder` is intended to be a `@dataclass`-like generator.
57
58
  The goal is to handle some of the basic functions and to allow for flexible
58
59
  customization of both the field collection and the method generation.
59
60
 
60
61
  `ducktools.classbuilder.prefab` includes a prebuilt implementation using these tools.
61
62
 
63
+ Install from PyPI with:
64
+ `python -m pip install ducktools-classbuilder`
65
+
66
+ ## Usage: building a class decorator ##
67
+
68
+ In order to create a class decorator using `ducktools.classbuilder` there are
69
+ a few things you need to prepare.
70
+
71
+ 1. A field gathering function to analyse the class and collect valid `Field`s.
72
+ * An example `slot_gatherer` is included.
73
+ 2. Code generators that can make use of the gathered `Field`s to create magic method
74
+ source code.
75
+ * Example `init_maker`, `repr_maker` and `eq_maker` generators are included.
76
+ 3. A function that calls the `builder` function to apply both of these steps.
77
+
78
+ A field gathering function needs to take the original class as an argument and
79
+ return a dictionary of `{key: Field(...)}` pairs.
80
+
81
+ > [!NOTE]
82
+ > The `builder` will handle inheritance so do not collect fields from parent classes.
83
+
84
+ The code generators take the class as the only argument and return a tuple
85
+ of method source code and globals to be provided to `exec(code, globs)` in order
86
+ to generate the actual method.
87
+
88
+ The provided `slot_gatherer` looks for `__slots__` being assigned a `SlotFields`
89
+ class[^1] where keyword arguments define the names and values for the fields.
90
+
91
+ Code generator functions need to be converted to descriptors before being used.
92
+ This is done using the provided `MethodMaker` descriptor class.
93
+ ex: `init_desc = MethodMaker("__init__", init_maker)`
94
+
95
+ These parts can then be used to make a basic class boilerplate generator by
96
+ providing them to the `builder` function.
97
+
98
+ ```python
99
+ from ducktools.classbuilder import (
100
+ builder,
101
+ slot_gatherer,
102
+ init_maker, eq_maker, repr_maker,
103
+ MethodMaker,
104
+ )
105
+
106
+ init_desc = MethodMaker("__init__", init_maker)
107
+ repr_desc = MethodMaker("__repr__", repr_maker)
108
+ eq_desc = MethodMaker("__eq__", eq_maker)
109
+
110
+ def slotclass(cls):
111
+ return builder(cls, gatherer=slot_gatherer, methods={init_desc, repr_desc, eq_desc})
112
+ ```
113
+
62
114
  ## Slot Class Usage ##
63
115
 
64
- The building toolkit also includes a basic implementation that uses
65
- `__slots__` to define the fields by assigning a `SlotFields` instance.
116
+ This created `slotclass` function can then be used as a decorator to generate classes in
117
+ a similar manner to the `@dataclass` decorator from `dataclasses`.
118
+
119
+ > [!NOTE]
120
+ > `ducktools.classbuilder` includes a premade version of `slotclass` that can
121
+ > be used directly. (The included version has some extra features).
66
122
 
67
123
  ```python
68
- from ducktools.classbuilder import slotclass, Field, SlotFields
124
+ from ducktools.classbuilder import Field, SlotFields
69
125
 
70
126
  @slotclass
71
127
  class SlottedDC:
@@ -81,28 +137,36 @@ ex = SlottedDC()
81
137
  print(ex)
82
138
  ```
83
139
 
84
- ## Why does the basic implementation use slots? ##
140
+ > [!TIP]
141
+ > For more information and examples of creating class generators with additional
142
+ > features using the builder see
143
+ > [the docs](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
144
+
145
+ ## Why does your example use `__slots__` instead of annotations? ##
85
146
 
86
- Dataclasses has a problem when you use `@dataclass(slots=True)`,
87
- although this is not unique to dataclasses but inherent to the way both
88
- `__slots__` and decorators work.
147
+ If you want to use `__slots__` in order to save memory you have to declare
148
+ them when the class is originally created as you can't add them later.
89
149
 
90
- In order for this to *appear* to work, dataclasses has to make a new class
91
- and attempt to copy over everything from the original. This is because
92
- decorators operate on classes *after they have been created* while slots
93
- need to be declared beforehand. While you can change the value of `__slots__`
94
- after a class has been created, this will have no effect on the internal
95
- structure of the class.
150
+ When you use `@dataclass(slots=True)`[^2] with `dataclasses` in order for
151
+ this to work, `dataclasses` has to make a new class and attempt to
152
+ copy over everything from the original.
153
+ This is because decorators operate on classes *after they have been created*
154
+ while slots need to be declared beforehand.
155
+ While you can change the value of `__slots__` after a class has been created,
156
+ this will have no effect on the internal structure of the class.
96
157
 
97
158
  By declaring the class using `__slots__` on the other hand, we can take
98
159
  advantage of the fact that it accepts a mapping, where the keys will be
99
160
  used as the attributes to create as slots. The values can then be used as
100
- the default values equivalently to how type hints are used in dataclasses.
161
+ the default values equivalently to how type hints are used in `dataclasses`.
101
162
 
102
- For example these two classes would be roughly equivalent, except
163
+ For example these two classes would be roughly equivalent, except that
103
164
  `@dataclass` has had to recreate the class from scratch while `@slotclass`
104
- has simply added the methods on to the original class. This is easy to
105
- demonstrate using another decorator.
165
+ has added the methods on to the original class.
166
+ This means that any references stored to the original class *before*
167
+ `@dataclass` has rebuilt the class will not be pointing towards the
168
+ correct class.
169
+ This can be demonstrated using a simple class register decorator.
106
170
 
107
171
  > This example requires Python 3.10 as earlier versions of
108
172
  > `dataclasses` did not support the `slots` argument.
@@ -140,7 +204,6 @@ print(SlotCoords())
140
204
 
141
205
  print(f"{DataCoords is class_register[DataCoords.__name__] = }")
142
206
  print(f"{SlotCoords is class_register[SlotCoords.__name__] = }")
143
-
144
207
  ```
145
208
 
146
209
  ## What features does this have? ##
@@ -158,9 +221,6 @@ field so they are present on the class if `help(...)` is called.
158
221
  If you want something with more features you can look at the `prefab.py`
159
222
  implementation which provides a 'prebuilt' implementation.
160
223
 
161
- For more information on creating class generators using the builder
162
- see [the docs](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
163
-
164
224
  ## Will you add \<feature\> to `classbuilder.prefab`? ##
165
225
 
166
226
  No. Not unless it's something I need or find interesting.
@@ -177,3 +237,9 @@ with a specific feature, you can create or add it yourself.
177
237
  ## Credit ##
178
238
 
179
239
  Heavily inspired by [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
240
+
241
+ [^1]: `SlotFields` is actually just a subclassed `dict` with no changes. `__slots__`
242
+ works with dictionaries using the values of the keys, while fields are normally
243
+ used for documentation.
244
+
245
+ [^2]: or `@attrs.define`.
@@ -0,0 +1,10 @@
1
+ ducktools/classbuilder/__init__.py,sha256=OOVk-E6UBi8-68bEy9pqCiKbn9oQ1y4tRGnZ3LGsGYg,13747
2
+ ducktools/classbuilder/__init__.pyi,sha256=-QHJhPn4EjI4XW4OI74MKNg2tGKJiM3w0ELSOvPstAw,2877
3
+ ducktools/classbuilder/prefab.py,sha256=yUa_yoPKU1hzI4klqWJOwTZEXtNOmUU1fQnLuHkVACw,29871
4
+ ducktools/classbuilder/prefab.pyi,sha256=4cyXYqTXtCJv4gPOSNtkBrjORj2jt1bvX6p_sCOzoXw,3963
5
+ ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
6
+ ducktools_classbuilder-0.2.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
7
+ ducktools_classbuilder-0.2.0.dist-info/METADATA,sha256=LmHMIhj4jJ9WlmHOac4lBVgW4l6cdNUjG8cEvIULVko,9280
8
+ ducktools_classbuilder-0.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
+ ducktools_classbuilder-0.2.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
10
+ ducktools_classbuilder-0.2.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- ducktools/classbuilder/__init__.py,sha256=oEdcQdMEqaQjUfbPkHpe55iCiPssDQxf4zxoAMdsTt8,12950
2
- ducktools/classbuilder/__init__.pyi,sha256=BlpHmxIL4dAiVKgqElRR3wVs7aw73qyJRaSvr-Tuyc0,2770
3
- ducktools/classbuilder/prefab.py,sha256=qQJzN4ys6Av6s9NaabzSqRKE5BXDfhidt3EtGZ0HAxQ,29405
4
- ducktools/classbuilder/prefab.pyi,sha256=wrq8NKwy9TsQ6fpnMyTf4DaGtCOK_90NHeU61CfNOo4,3589
5
- ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
6
- ducktools_classbuilder-0.1.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
7
- ducktools_classbuilder-0.1.0.dist-info/METADATA,sha256=FHgeLZx-RfcvY2ty1l6O19z449So-UQZIcBinJ8n5xU,6675
8
- ducktools_classbuilder-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
- ducktools_classbuilder-0.1.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
10
- ducktools_classbuilder-0.1.0.dist-info/RECORD,,