ducktools-classbuilder 0.3.0__py3-none-any.whl → 0.4.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,12 +19,19 @@
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.3.0"
22
+ import sys
23
+
24
+ __version__ = "v0.4.0"
23
25
 
24
26
  # Change this name if you make heavy modifications
25
27
  INTERNALS_DICT = "__classbuilder_internals__"
26
28
 
27
29
 
30
+ # If testing, make Field classes frozen to make sure attributes are not
31
+ # overwritten. When running this is a performance penalty so it is not required.
32
+ _UNDER_TESTING = "pytest" in sys.modules
33
+
34
+
28
35
  def get_fields(cls, *, local=False):
29
36
  """
30
37
  Utility function to gather the fields dictionary
@@ -102,46 +109,52 @@ class MethodMaker:
102
109
  return method.__get__(instance, cls)
103
110
 
104
111
 
105
- def init_maker(cls, *, null=NOTHING, extra_code=None):
106
- fields = get_fields(cls)
107
- flags = get_flags(cls)
112
+ def get_init_maker(null=NOTHING, extra_code=None):
113
+ def cls_init_maker(cls):
114
+ fields = get_fields(cls)
115
+ flags = get_flags(cls)
108
116
 
109
- arglist = []
110
- assignments = []
111
- globs = {}
117
+ arglist = []
118
+ assignments = []
119
+ globs = {}
112
120
 
113
- if flags.get("kw_only", False):
114
- arglist.append("*")
121
+ if flags.get("kw_only", False):
122
+ arglist.append("*")
115
123
 
116
- for k, v in fields.items():
117
- if v.default is not null:
118
- globs[f"_{k}_default"] = v.default
119
- arg = f"{k}=_{k}_default"
120
- assignment = f"self.{k} = {k}"
121
- elif v.default_factory is not null:
122
- globs[f"_{k}_factory"] = v.default_factory
123
- arg = f"{k}=None"
124
- assignment = f"self.{k} = _{k}_factory() if {k} is None else {k}"
125
- else:
126
- arg = f"{k}"
127
- assignment = f"self.{k} = {k}"
124
+ for k, v in fields.items():
125
+ if v.default is not null:
126
+ globs[f"_{k}_default"] = v.default
127
+ arg = f"{k}=_{k}_default"
128
+ assignment = f"self.{k} = {k}"
129
+ elif v.default_factory is not null:
130
+ globs[f"_{k}_factory"] = v.default_factory
131
+ arg = f"{k}=None"
132
+ assignment = f"self.{k} = _{k}_factory() if {k} is None else {k}"
133
+ else:
134
+ arg = f"{k}"
135
+ assignment = f"self.{k} = {k}"
128
136
 
129
- arglist.append(arg)
130
- assignments.append(assignment)
137
+ arglist.append(arg)
138
+ assignments.append(assignment)
131
139
 
132
- args = ", ".join(arglist)
133
- assigns = "\n ".join(assignments) if assignments else "pass\n"
134
- code = (
135
- f"def __init__(self, {args}):\n"
136
- f" {assigns}\n"
137
- )
138
- # Handle additional function calls
139
- # Used for validate_field on fieldclasses
140
- if extra_code:
141
- for line in extra_code:
142
- code += f" {line}\n"
140
+ args = ", ".join(arglist)
141
+ assigns = "\n ".join(assignments) if assignments else "pass\n"
142
+ code = (
143
+ f"def __init__(self, {args}):\n"
144
+ f" {assigns}\n"
145
+ )
146
+ # Handle additional function calls
147
+ # Used for validate_field on fieldclasses
148
+ if extra_code:
149
+ for line in extra_code:
150
+ code += f" {line}\n"
143
151
 
144
- return code, globs
152
+ return code, globs
153
+
154
+ return cls_init_maker
155
+
156
+
157
+ init_maker = get_init_maker()
145
158
 
146
159
 
147
160
  def repr_maker(cls):
@@ -178,11 +191,54 @@ def eq_maker(cls):
178
191
  return code, globs
179
192
 
180
193
 
194
+ def frozen_setattr_maker(cls):
195
+ globs = {}
196
+ field_names = set(get_fields(cls))
197
+ flags = get_flags(cls)
198
+
199
+ globs["__field_names"] = field_names
200
+
201
+ # Better to be safe and use the method that works in both cases
202
+ # if somehow slotted has not been set.
203
+ if flags.get("slotted", True):
204
+ globs["__setattr_func"] = object.__setattr__
205
+ setattr_method = "__setattr_func(self, name, value)"
206
+ else:
207
+ setattr_method = "self.__dict__[name] = value"
208
+
209
+ body = (
210
+ f" if hasattr(self, name) or name not in __field_names:\n"
211
+ f' raise TypeError(\n'
212
+ f' f"{{type(self).__name__!r}} object does not support "'
213
+ f' f"attribute assignment"\n'
214
+ f' )\n'
215
+ f" else:\n"
216
+ f" {setattr_method}\n"
217
+ )
218
+ code = f"def __setattr__(self, name, value):\n{body}"
219
+
220
+ return code, globs
221
+
222
+
223
+ def frozen_delattr_maker(cls):
224
+ body = (
225
+ ' raise TypeError(\n'
226
+ ' f"{type(self).__name__!r} object "\n'
227
+ ' f"does not support attribute deletion"\n'
228
+ ' )\n'
229
+ )
230
+ code = f"def __delattr__(self, name):\n{body}"
231
+ globs = {}
232
+ return code, globs
233
+
234
+
181
235
  # As only the __get__ method refers to the class we can use the same
182
236
  # Descriptor instances for every class.
183
237
  init_desc = MethodMaker("__init__", init_maker)
184
238
  repr_desc = MethodMaker("__repr__", repr_maker)
185
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)
186
242
  default_methods = frozenset({init_desc, repr_desc, eq_desc})
187
243
 
188
244
 
@@ -244,6 +300,8 @@ class Field:
244
300
  some metadata.
245
301
 
246
302
  Intended to be extendable by subclasses for additional features.
303
+
304
+ Note: When run under `pytest`, Field instances are Frozen.
247
305
  """
248
306
  __slots__ = {
249
307
  "default": "Standard default value to be used for attributes with"
@@ -303,14 +361,19 @@ _field_internal = {
303
361
  "doc": Field(default=None),
304
362
  }
305
363
 
364
+ _field_methods = {repr_desc, eq_desc}
365
+ if _UNDER_TESTING:
366
+ _field_methods.update({frozen_setattr_desc, frozen_delattr_desc})
367
+
306
368
  builder(
307
369
  Field,
308
370
  gatherer=lambda cls_: _field_internal,
309
- methods=frozenset({repr_desc, eq_desc}),
371
+ methods=_field_methods,
310
372
  flags={"slotted": True, "kw_only": True},
311
373
  )
312
374
 
313
375
 
376
+ # Slot gathering tools
314
377
  # Subclass of dict to be identifiable by isinstance checks
315
378
  # For anything more complicated this could be made into a Mapping
316
379
  class SlotFields(dict):
@@ -326,46 +389,152 @@ class SlotFields(dict):
326
389
  """
327
390
 
328
391
 
329
- def slot_gatherer(cls):
392
+ def make_slot_gatherer(field_type=Field):
393
+ def field_slot_gatherer(cls):
394
+ """
395
+ Gather field information for class generation based on __slots__
396
+
397
+ :param cls: Class to gather field information from
398
+ :return: dict of field_name: Field(...)
399
+ """
400
+ cls_slots = cls.__dict__.get("__slots__", None)
401
+
402
+ if not isinstance(cls_slots, SlotFields):
403
+ raise TypeError(
404
+ "__slots__ must be an instance of SlotFields "
405
+ "in order to generate a slotclass"
406
+ )
407
+
408
+ cls_annotations = cls.__dict__.get("__annotations__", {})
409
+ cls_fields = {}
410
+ slot_replacement = {}
411
+
412
+ for k, v in cls_slots.items():
413
+ if isinstance(v, field_type):
414
+ attrib = v
415
+ if attrib.type is not NOTHING:
416
+ cls_annotations[k] = attrib.type
417
+ else:
418
+ # Plain values treated as defaults
419
+ attrib = field_type(default=v)
420
+
421
+ slot_replacement[k] = attrib.doc
422
+ cls_fields[k] = attrib
423
+
424
+ # Replace the SlotAttributes instance with a regular dict
425
+ # So that help() works
426
+ setattr(cls, "__slots__", slot_replacement)
427
+
428
+ # Update annotations with any types from the slots assignment
429
+ setattr(cls, "__annotations__", cls_annotations)
430
+ return cls_fields
431
+
432
+ return field_slot_gatherer
433
+
434
+
435
+ slot_gatherer = make_slot_gatherer()
436
+
437
+
438
+ # Annotation gathering tools
439
+ def is_classvar(hint):
440
+ _typing = sys.modules.get("typing")
441
+
442
+ if _typing:
443
+ # Annotated is a nightmare I'm never waking up from
444
+ # 3.8 and 3.9 need Annotated from typing_extensions
445
+ # 3.8 also needs get_origin from typing_extensions
446
+ if sys.version_info < (3, 10):
447
+ _typing_extensions = sys.modules.get("typing_extensions")
448
+ if _typing_extensions:
449
+ _Annotated = _typing_extensions.Annotated
450
+ _get_origin = _typing_extensions.get_origin
451
+ else:
452
+ _Annotated, _get_origin = None, None
453
+ else:
454
+ _Annotated = _typing.Annotated
455
+ _get_origin = _typing.get_origin
456
+
457
+ if _Annotated and _get_origin(hint) is _Annotated:
458
+ hint = getattr(hint, "__origin__", None)
459
+
460
+ if (
461
+ hint is _typing.ClassVar
462
+ or getattr(hint, "__origin__", None) is _typing.ClassVar
463
+ ):
464
+ return True
465
+ # String used as annotation
466
+ elif isinstance(hint, str) and "ClassVar" in hint:
467
+ return True
468
+ return False
469
+
470
+
471
+ def make_annotation_gatherer(field_type=Field, leave_default_values=True):
330
472
  """
331
- Gather field information for class generation based on __slots__
332
-
333
- :param cls: Class to gather field information from
334
- :return: dict of field_name: Field(...)
473
+ Create a new annotation gatherer that will work with `Field` instances
474
+ of the creators definition.
475
+
476
+ :param field_type: The `Field` classes to be used when gathering fields
477
+ :param leave_default_values: Set to True if the gatherer should leave
478
+ default values in place as class variables.
479
+ :return: An annotation gatherer with these settings.
335
480
  """
336
- cls_slots = cls.__dict__.get("__slots__", None)
481
+ def field_annotation_gatherer(cls):
482
+ cls_annotations = cls.__dict__.get("__annotations__", {})
483
+
484
+ cls_fields: dict[str, field_type] = {}
485
+
486
+ for k, v in cls_annotations.items():
487
+ # Ignore ClassVar
488
+ if is_classvar(v):
489
+ continue
490
+
491
+ attrib = getattr(cls, k, NOTHING)
492
+
493
+ if attrib is not NOTHING:
494
+ if isinstance(attrib, field_type):
495
+ attrib = field_type.from_field(attrib, type=v)
496
+ if attrib.default is not NOTHING and leave_default_values:
497
+ setattr(cls, k, attrib.default)
498
+ else:
499
+ delattr(cls, k)
500
+ else:
501
+ attrib = field_type(default=attrib, type=v)
502
+ if not leave_default_values:
503
+ delattr(cls, k)
337
504
 
338
- if not isinstance(cls_slots, SlotFields):
339
- raise TypeError(
340
- "__slots__ must be an instance of SlotFields "
341
- "in order to generate a slotclass"
342
- )
505
+ else:
506
+ attrib = field_type(type=v)
343
507
 
344
- cls_annotations = cls.__dict__.get("__annotations__", {})
345
- cls_fields = {}
346
- slot_replacement = {}
508
+ cls_fields[k] = attrib
347
509
 
348
- for k, v in cls_slots.items():
349
- if isinstance(v, Field):
350
- attrib = v
351
- if v.type is not NOTHING:
352
- cls_annotations[k] = attrib.type
353
- else:
354
- # Plain values treated as defaults
355
- attrib = Field(default=v)
510
+ return cls_fields
511
+
512
+ return field_annotation_gatherer
356
513
 
357
- slot_replacement[k] = attrib.doc
358
- cls_fields[k] = attrib
359
514
 
360
- # Replace the SlotAttributes instance with a regular dict
361
- # So that help() works
362
- setattr(cls, "__slots__", slot_replacement)
515
+ annotation_gatherer = make_annotation_gatherer()
363
516
 
364
- # Update annotations with any types from the slots assignment
365
- setattr(cls, "__annotations__", cls_annotations)
366
- return cls_fields
367
517
 
518
+ def check_argument_order(cls):
519
+ """
520
+ Raise a SyntaxError if the argument order will be invalid for a generated
521
+ `__init__` function.
522
+
523
+ :param cls: class being built
524
+ """
525
+ fields = get_fields(cls)
526
+ used_default = False
527
+ for k, v in fields.items():
528
+ if v.default is NOTHING and v.default_factory is NOTHING:
529
+ if used_default:
530
+ raise SyntaxError(
531
+ f"non-default argument {k!r} follows default argument"
532
+ )
533
+ else:
534
+ used_default = True
368
535
 
536
+
537
+ # Class Decorators
369
538
  def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
370
539
  """
371
540
  Example of class builder in action using __slots__ to find fields.
@@ -382,40 +551,50 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
382
551
  cls = builder(cls, gatherer=slot_gatherer, methods=methods, flags={"slotted": True})
383
552
 
384
553
  if syntax_check:
385
- fields = get_fields(cls)
386
- used_default = False
387
- for k, v in fields.items():
388
- if v.default is NOTHING and v.default_factory is NOTHING:
389
- if used_default:
390
- raise SyntaxError(
391
- f"non-default argument {k!r} follows default argument"
392
- )
393
- else:
394
- used_default = True
554
+ check_argument_order(cls)
395
555
 
396
556
  return cls
397
557
 
398
558
 
399
- def _field_init_func(cls):
400
- # Fields need a different Nothing for their __init__ generation
401
- # And an extra call to validate_field
402
- field_nothing = _NothingType()
403
- extra_calls = ["self.validate_field()"]
404
- return init_maker(cls, null=field_nothing, extra_code=extra_calls)
559
+ def annotationclass(cls=None, /, *, methods=default_methods):
560
+ if not cls:
561
+ return lambda cls_: annotationclass(cls_, methods=methods)
405
562
 
563
+ cls = builder(cls, gatherer=annotation_gatherer, methods=methods, flags={"slotted": False})
406
564
 
407
- def fieldclass(cls):
565
+ check_argument_order(cls)
566
+
567
+ return cls
568
+
569
+
570
+ _field_init_desc = MethodMaker(
571
+ funcname="__init__",
572
+ code_generator=get_init_maker(
573
+ null=_NothingType(),
574
+ extra_code=["self.validate_field()"],
575
+ )
576
+ )
577
+
578
+
579
+ def fieldclass(cls=None, /, *, frozen=False):
408
580
  """
409
581
  This is a special decorator for making Field subclasses using __slots__.
410
582
  This works by forcing the __init__ method to treat NOTHING as a regular
411
583
  value. This means *all* instance attributes always have defaults.
412
584
 
413
585
  :param cls: Field subclass
586
+ :param frozen: Make the field class a frozen class.
587
+ Field classes are always frozen when running under `pytest`
414
588
  :return: Modified subclass
415
589
  """
590
+ if not cls:
591
+ return lambda cls_: fieldclass(cls_, frozen=frozen)
592
+
593
+ field_methods = {_field_init_desc, repr_desc, eq_desc}
416
594
 
417
- field_init_desc = MethodMaker("__init__", _field_init_func)
418
- field_methods = frozenset({field_init_desc, repr_desc, eq_desc})
595
+ # Always freeze when running tests
596
+ if frozen or _UNDER_TESTING:
597
+ field_methods.update({frozen_setattr_desc, frozen_delattr_desc})
419
598
 
420
599
  cls = builder(
421
600
  cls,
@@ -26,18 +26,24 @@ class MethodMaker:
26
26
  def __repr__(self) -> str: ...
27
27
  def __get__(self, instance, cls) -> Callable: ...
28
28
 
29
- def init_maker(
30
- cls: type,
31
- *,
29
+ def get_init_maker(
32
30
  null: _NothingType = NOTHING,
33
31
  extra_code: None | list[str] = None
34
- ) -> tuple[str, dict[str, typing.Any]]: ...
32
+ ) -> Callable[[type], tuple[str, dict[str, typing.Any]]]: ...
33
+
34
+ def init_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
35
35
  def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
36
36
  def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
37
37
 
38
+ def frozen_setattr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
39
+
40
+ def frozen_delattr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
41
+
38
42
  init_desc: MethodMaker
39
43
  repr_desc: MethodMaker
40
44
  eq_desc: MethodMaker
45
+ frozen_setattr_desc: MethodMaker
46
+ frozen_delattr_desc: MethodMaker
41
47
  default_methods: frozenset[MethodMaker]
42
48
 
43
49
  _T = typing.TypeVar("_T")
@@ -89,9 +95,22 @@ class Field:
89
95
  class SlotFields(dict):
90
96
  ...
91
97
 
98
+ def make_slot_gatherer(field_type: type[Field] = Field) -> Callable[[type], dict[str, Field]]: ...
99
+
92
100
  def slot_gatherer(cls: type) -> dict[str, Field]:
93
101
  ...
94
102
 
103
+ def is_classvar(hint: object) -> bool: ...
104
+
105
+ def make_annotation_gatherer(
106
+ field_type: type[Field] = Field,
107
+ leave_default_values: bool = True,
108
+ ) -> Callable[[type], dict[str, Field]]: ...
109
+
110
+ def annotation_gatherer(cls: type) -> dict[str, Field]: ...
111
+
112
+ def check_argument_order(cls: type) -> None: ...
113
+
95
114
  @typing.overload
96
115
  def slotclass(
97
116
  cls: type[_T],
@@ -110,4 +129,24 @@ def slotclass(
110
129
  syntax_check: bool = True
111
130
  ) -> Callable[[type[_T]], type[_T]]: ...
112
131
 
113
- def fieldclass(cls: type[_T]) -> type[_T]: ...
132
+ @typing.overload
133
+ def annotationclass(
134
+ cls: type[_T],
135
+ /,
136
+ *,
137
+ methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
138
+ ) -> type[_T]: ...
139
+
140
+ @typing.overload
141
+ def annotationclass(
142
+ cls: None = None,
143
+ /,
144
+ *,
145
+ methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
146
+ ) -> Callable[[type[_T]], type[_T]]: ...
147
+
148
+ @typing.overload
149
+ def fieldclass(cls: type[_T], /, *, frozen: bool = False) -> type[_T]: ...
150
+
151
+ @typing.overload
152
+ def fieldclass(cls: None = None, /, *, frozen: bool = False) -> Callable[[type[_T]], type[_T]]: ...
@@ -31,7 +31,8 @@ import sys
31
31
  from . import (
32
32
  INTERNALS_DICT, NOTHING,
33
33
  Field, MethodMaker, SlotFields,
34
- builder, fieldclass, get_flags, get_fields, slot_gatherer
34
+ builder, fieldclass, get_flags, get_fields, make_slot_gatherer,
35
+ frozen_setattr_desc, frozen_delattr_desc, is_classvar,
35
36
  )
36
37
 
37
38
  PREFAB_FIELDS = "PREFAB_FIELDS"
@@ -55,20 +56,6 @@ class PrefabError(Exception):
55
56
  pass
56
57
 
57
58
 
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
59
  def get_attributes(cls):
73
60
  """
74
61
  Copy of get_fields, typed to return Attribute instead of Field.
@@ -340,57 +327,6 @@ def get_iter_maker():
340
327
  return MethodMaker("__iter__", __iter__)
341
328
 
342
329
 
343
- def get_frozen_setattr_maker():
344
- def __setattr__(cls: "type") -> "tuple[str, dict]":
345
- globs = {}
346
- attributes = get_attributes(cls)
347
- flags = get_flags(cls)
348
-
349
- # Make the fields set literal
350
- fields_delimited = ", ".join(f"{field!r}" for field in attributes)
351
- field_set = f"{{ {fields_delimited} }}"
352
-
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):
356
- globs["__prefab_setattr_func"] = object.__setattr__
357
- setattr_method = "__prefab_setattr_func(self, name, value)"
358
- else:
359
- setattr_method = "self.__dict__[name] = value"
360
-
361
- body = (
362
- f" if hasattr(self, name) or name not in {field_set}:\n"
363
- f' raise TypeError(\n'
364
- f' f"{{type(self).__name__!r}} object does not support "'
365
- f' f"attribute assignment"\n'
366
- f' )\n'
367
- f" else:\n"
368
- f" {setattr_method}\n"
369
- )
370
- code = f"def __setattr__(self, name, value):\n{body}"
371
-
372
- return code, globs
373
-
374
- # Pass the exception to exec
375
- return MethodMaker("__setattr__", __setattr__)
376
-
377
-
378
- def get_frozen_delattr_maker():
379
- # noinspection PyUnusedLocal
380
- def __delattr__(cls: "type") -> "tuple[str, dict]":
381
- body = (
382
- ' raise TypeError(\n'
383
- ' f"{type(self).__name__!r} object "\n'
384
- ' f"does not support attribute deletion"\n'
385
- ' )\n'
386
- )
387
- code = f"def __delattr__(self, name):\n{body}"
388
- globs = {}
389
- return code, globs
390
-
391
- return MethodMaker("__delattr__", __delattr__)
392
-
393
-
394
330
  def get_asdict_maker():
395
331
  def as_dict_gen(cls: "type") -> "tuple[str, dict]":
396
332
  fields = get_attributes(cls)
@@ -414,8 +350,6 @@ repr_desc = get_repr_maker()
414
350
  recursive_repr_desc = get_repr_maker(recursion_safe=True)
415
351
  eq_desc = get_eq_maker()
416
352
  iter_desc = get_iter_maker()
417
- frozen_setattr_desc = get_frozen_setattr_maker()
418
- frozen_delattr_desc = get_frozen_delattr_maker()
419
353
  asdict_desc = get_asdict_maker()
420
354
 
421
355
 
@@ -491,12 +425,7 @@ def attribute(
491
425
  )
492
426
 
493
427
 
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
- }
428
+ slot_prefab_gatherer = make_slot_gatherer(Attribute)
500
429
 
501
430
 
502
431
  # Gatherer for classes built on attributes or annotations
@@ -519,7 +448,7 @@ def attribute_gatherer(cls):
519
448
  new_attributes = {}
520
449
  for name, value in cls_annotations.items():
521
450
  # Ignore ClassVar hints
522
- if _is_classvar(value):
451
+ if is_classvar(value):
523
452
  continue
524
453
 
525
454
  # Look for the KW_ONLY annotation
@@ -533,37 +462,43 @@ def attribute_gatherer(cls):
533
462
  # Copy attributes that are already defined to the new dict
534
463
  # generate Attribute() values for those that are not defined.
535
464
 
465
+ # Extra parameters to pass to each Attribute
466
+ extras = {
467
+ "type": cls_annotations[name]
468
+ }
469
+ if kw_flag:
470
+ extras["kw_only"] = True
471
+
536
472
  # If a field name is also declared in slots it can't have a real
537
473
  # default value and the attr will be the slot descriptor.
538
474
  if hasattr(cls, name) and name not in cls_slots:
539
475
  if name in cls_attribute_names:
540
- attrib = cls_attributes[name]
476
+ attrib = Attribute.from_field(
477
+ cls_attributes[name],
478
+ **extras,
479
+ )
541
480
  else:
542
481
  attribute_default = getattr(cls, name)
543
- attrib = attribute(default=attribute_default)
482
+ attrib = attribute(default=attribute_default, **extras)
544
483
 
545
484
  # Clear the attribute from the class after it has been used
546
485
  # in the definition.
547
486
  delattr(cls, name)
548
487
  else:
549
- attrib = attribute()
488
+ attrib = attribute(**extras)
550
489
 
551
- if kw_flag:
552
- attrib.kw_only = True
553
-
554
- attrib.type = cls_annotations[name]
555
490
  new_attributes[name] = attrib
556
491
 
557
492
  cls_attributes = new_attributes
558
493
  else:
559
- for name, attrib in cls_attributes.items():
560
- delattr(cls, name)
494
+ for name in cls_attributes.keys():
495
+ attrib = cls_attributes[name]
496
+ delattr(cls, name) # clear attrib from class
561
497
 
562
498
  # Some items can still be annotated.
563
- try:
564
- attrib.type = cls_annotations[name]
565
- except KeyError:
566
- pass
499
+ if name in cls_annotations:
500
+ new_attrib = Attribute.from_field(attrib, type=cls_annotations[name])
501
+ cls_attributes[name] = new_attrib
567
502
 
568
503
  return cls_attributes
569
504
 
@@ -26,8 +26,6 @@ KW_ONLY: _KW_ONLY_TYPE
26
26
 
27
27
  class PrefabError(Exception): ...
28
28
 
29
- def _is_classvar(hint: type | str) -> bool: ...
30
-
31
29
  def get_attributes(cls: type) -> dict[str, Attribute]: ...
32
30
 
33
31
  def get_init_maker(*, init_name: str="__init__") -> MethodMaker: ...
@@ -38,10 +36,6 @@ def get_eq_maker() -> MethodMaker: ...
38
36
 
39
37
  def get_iter_maker() -> MethodMaker: ...
40
38
 
41
- def get_frozen_setattr_maker() -> MethodMaker: ...
42
-
43
- def get_frozen_delattr_maker() -> MethodMaker: ...
44
-
45
39
  def get_asdict_maker() -> MethodMaker: ...
46
40
 
47
41
 
@@ -51,8 +45,6 @@ repr_desc: MethodMaker
51
45
  recursive_repr_desc: MethodMaker
52
46
  eq_desc: MethodMaker
53
47
  iter_desc: MethodMaker
54
- frozen_setattr_desc: MethodMaker
55
- frozen_delattr_desc: MethodMaker
56
48
  asdict_desc: MethodMaker
57
49
 
58
50
  class Attribute(Field):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -45,6 +45,7 @@ Provides-Extra: testing
45
45
  Requires-Dist: pytest ; extra == 'testing'
46
46
  Requires-Dist: pytest-cov ; extra == 'testing'
47
47
  Requires-Dist: mypy ; extra == 'testing'
48
+ Requires-Dist: typing-extensions ; (python_version < "3.10") and extra == 'testing'
48
49
 
49
50
  # Ducktools: Class Builder #
50
51
 
@@ -121,7 +122,7 @@ a similar manner to the `@dataclass` decorator from `dataclasses`.
121
122
  > be used directly. (The included version has some extra features).
122
123
 
123
124
  ```python
124
- from ducktools.classbuilder import Field, SlotFields
125
+ from ducktools.classbuilder import Field, SlotFields, slotclass
125
126
 
126
127
  @slotclass
127
128
  class SlottedDC:
@@ -206,6 +207,25 @@ print(f"{DataCoords is class_register[DataCoords.__name__] = }")
206
207
  print(f"{SlotCoords is class_register[SlotCoords.__name__] = }")
207
208
  ```
208
209
 
210
+ ## Using annotations anyway ##
211
+
212
+ For those that really want to use type annotations a basic `annotation_gatherer`
213
+ function and `@annotationclass` decorator are also included. Slots are not generated
214
+ in this case.
215
+
216
+ ```python
217
+ from ducktools.classbuilder import annotationclass
218
+
219
+ @annotationclass
220
+ class AnnotatedDC:
221
+ the_answer: int = 42
222
+ the_question: str = "What do you get if you multiply six by nine?"
223
+
224
+
225
+ ex = AnnotatedDC()
226
+ print(ex)
227
+ ```
228
+
209
229
  ## What features does this have? ##
210
230
 
211
231
  Included as an example implementation, the `slotclass` generator supports
@@ -218,6 +238,10 @@ It will copy values provided as the `type` to `Field` into the
218
238
  Values provided to `doc` will be placed in the final `__slots__`
219
239
  field so they are present on the class if `help(...)` is called.
220
240
 
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.
244
+
221
245
  If you want something with more features you can look at the `prefab.py`
222
246
  implementation which provides a 'prebuilt' implementation.
223
247
 
@@ -0,0 +1,10 @@
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,,
@@ -1,10 +0,0 @@
1
- ducktools/classbuilder/__init__.py,sha256=Vw5RjzzIkXGYXKOmFWTxPsIAM4ceg74Q1Oku4fwF0ek,13505
2
- ducktools/classbuilder/__init__.pyi,sha256=hgcEylMAHgfonOt2fH9OCE3gU60mYkiOUe7e8mTwNVs,2857
3
- ducktools/classbuilder/prefab.py,sha256=gJJCtTAbQLgIoo79jRWVp09r3u8kJjcqXp4G4FTu9j8,29887
4
- ducktools/classbuilder/prefab.pyi,sha256=vcaEpBxyVsOTwkGBkVgmbpn8DPqQqFnAU4xTKPnpOd8,3977
5
- ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
6
- ducktools_classbuilder-0.3.0.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
7
- ducktools_classbuilder-0.3.0.dist-info/METADATA,sha256=sAWw76LgN3HruCS-SXURrrAj2A6MmhsyDJwMSvnWW2w,9280
8
- ducktools_classbuilder-0.3.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
- ducktools_classbuilder-0.3.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
10
- ducktools_classbuilder-0.3.0.dist-info/RECORD,,