ducktools-classbuilder 0.2.1__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,32 +19,17 @@
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.2.1"
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
 
28
- def get_internals(cls):
29
- """
30
- Utility function to get the internals dictionary
31
- or return None.
32
-
33
- As generated classes will always have 'fields'
34
- and 'local_fields' attributes this will always
35
- evaluate as 'truthy' if this is a generated class.
36
-
37
- Generally you should use the helper get_flags and
38
- get_fields methods.
39
-
40
- Usage:
41
- if internals := get_internals(cls):
42
- ...
43
-
44
- :param cls: generated class
45
- :return: internals dictionary of the class or None
46
- """
47
- return getattr(cls, INTERNALS_DICT, None)
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
48
33
 
49
34
 
50
35
  def get_fields(cls, *, local=False):
@@ -71,7 +56,9 @@ def get_flags(cls):
71
56
  return getattr(cls, INTERNALS_DICT)["flags"]
72
57
 
73
58
 
74
- def get_inst_fields(inst):
59
+ def _get_inst_fields(inst):
60
+ # This is an internal helper for constructing new
61
+ # 'Field' instances from existing ones.
75
62
  return {
76
63
  k: getattr(inst, k)
77
64
  for k in get_fields(type(inst))
@@ -105,7 +92,7 @@ class MethodMaker:
105
92
  self.code_generator = code_generator
106
93
 
107
94
  def __repr__(self):
108
- return f"<MethodMaker for {self.funcname} method>"
95
+ return f"<MethodMaker for {self.funcname!r} method>"
109
96
 
110
97
  def __get__(self, instance, cls):
111
98
  local_vars = {}
@@ -122,37 +109,52 @@ class MethodMaker:
122
109
  return method.__get__(instance, cls)
123
110
 
124
111
 
125
- def init_maker(cls, *, null=NOTHING):
126
- fields = get_fields(cls)
127
- 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)
128
116
 
129
- arglist = []
130
- assignments = []
131
- globs = {}
117
+ arglist = []
118
+ assignments = []
119
+ globs = {}
132
120
 
133
- if flags.get("kw_only", False):
134
- arglist.append("*")
121
+ if flags.get("kw_only", False):
122
+ arglist.append("*")
135
123
 
136
- for k, v in fields.items():
137
- if v.default is not null:
138
- globs[f"_{k}_default"] = v.default
139
- arg = f"{k}=_{k}_default"
140
- assignment = f"self.{k} = {k}"
141
- elif v.default_factory is not null:
142
- globs[f"_{k}_factory"] = v.default_factory
143
- arg = f"{k}=None"
144
- assignment = f"self.{k} = _{k}_factory() if {k} is None else {k}"
145
- else:
146
- arg = f"{k}"
147
- 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}"
148
136
 
149
- arglist.append(arg)
150
- assignments.append(assignment)
137
+ arglist.append(arg)
138
+ assignments.append(assignment)
151
139
 
152
- args = ", ".join(arglist)
153
- assigns = "\n ".join(assignments)
154
- code = f"def __init__(self, {args}):\n" f" {assigns}\n"
155
- return code, globs
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"
151
+
152
+ return code, globs
153
+
154
+ return cls_init_maker
155
+
156
+
157
+ init_maker = get_init_maker()
156
158
 
157
159
 
158
160
  def repr_maker(cls):
@@ -189,11 +191,54 @@ def eq_maker(cls):
189
191
  return code, globs
190
192
 
191
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
+
192
235
  # As only the __get__ method refers to the class we can use the same
193
236
  # Descriptor instances for every class.
194
237
  init_desc = MethodMaker("__init__", init_maker)
195
238
  repr_desc = MethodMaker("__repr__", repr_maker)
196
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)
197
242
  default_methods = frozenset({init_desc, repr_desc, eq_desc})
198
243
 
199
244
 
@@ -255,6 +300,8 @@ class Field:
255
300
  some metadata.
256
301
 
257
302
  Intended to be extendable by subclasses for additional features.
303
+
304
+ Note: When run under `pytest`, Field instances are Frozen.
258
305
  """
259
306
  __slots__ = {
260
307
  "default": "Standard default value to be used for attributes with"
@@ -300,7 +347,7 @@ class Field:
300
347
  :param kwargs: Additional keyword arguments for subclasses
301
348
  :return: new field subclass instance
302
349
  """
303
- argument_dict = {**get_inst_fields(fld), **kwargs}
350
+ argument_dict = {**_get_inst_fields(fld), **kwargs}
304
351
 
305
352
  return cls(**argument_dict)
306
353
 
@@ -314,14 +361,19 @@ _field_internal = {
314
361
  "doc": Field(default=None),
315
362
  }
316
363
 
364
+ _field_methods = {repr_desc, eq_desc}
365
+ if _UNDER_TESTING:
366
+ _field_methods.update({frozen_setattr_desc, frozen_delattr_desc})
367
+
317
368
  builder(
318
369
  Field,
319
370
  gatherer=lambda cls_: _field_internal,
320
- methods=frozenset({repr_desc, eq_desc}),
371
+ methods=_field_methods,
321
372
  flags={"slotted": True, "kw_only": True},
322
373
  )
323
374
 
324
375
 
376
+ # Slot gathering tools
325
377
  # Subclass of dict to be identifiable by isinstance checks
326
378
  # For anything more complicated this could be made into a Mapping
327
379
  class SlotFields(dict):
@@ -337,46 +389,152 @@ class SlotFields(dict):
337
389
  """
338
390
 
339
391
 
340
- 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):
341
472
  """
342
- Gather field information for class generation based on __slots__
343
-
344
- :param cls: Class to gather field information from
345
- :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.
346
480
  """
347
- 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)
348
504
 
349
- if not isinstance(cls_slots, SlotFields):
350
- raise TypeError(
351
- "__slots__ must be an instance of SlotFields "
352
- "in order to generate a slotclass"
353
- )
505
+ else:
506
+ attrib = field_type(type=v)
354
507
 
355
- cls_annotations = cls.__dict__.get("__annotations__", {})
356
- cls_fields = {}
357
- slot_replacement = {}
508
+ cls_fields[k] = attrib
509
+
510
+ return cls_fields
511
+
512
+ return field_annotation_gatherer
358
513
 
359
- for k, v in cls_slots.items():
360
- if isinstance(v, Field):
361
- attrib = v
362
- if v.type is not NOTHING:
363
- cls_annotations[k] = attrib.type
364
- else:
365
- # Plain values treated as defaults
366
- attrib = Field(default=v)
367
514
 
368
- slot_replacement[k] = attrib.doc
369
- cls_fields[k] = attrib
515
+ annotation_gatherer = make_annotation_gatherer()
370
516
 
371
- # Replace the SlotAttributes instance with a regular dict
372
- # So that help() works
373
- setattr(cls, "__slots__", slot_replacement)
374
517
 
375
- # Update annotations with any types from the slots assignment
376
- setattr(cls, "__annotations__", cls_annotations)
377
- return cls_fields
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
378
535
 
379
536
 
537
+ # Class Decorators
380
538
  def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
381
539
  """
382
540
  Example of class builder in action using __slots__ to find fields.
@@ -393,43 +551,50 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
393
551
  cls = builder(cls, gatherer=slot_gatherer, methods=methods, flags={"slotted": True})
394
552
 
395
553
  if syntax_check:
396
- fields = get_fields(cls)
397
- used_default = False
398
- for k, v in fields.items():
399
- if v.default is NOTHING and v.default_factory is NOTHING:
400
- if used_default:
401
- raise SyntaxError(
402
- f"non-default argument {k!r} follows default argument"
403
- )
404
- else:
405
- used_default = True
554
+ check_argument_order(cls)
406
555
 
407
556
  return cls
408
557
 
409
558
 
410
- def fieldclass(cls):
559
+ def annotationclass(cls=None, /, *, methods=default_methods):
560
+ if not cls:
561
+ return lambda cls_: annotationclass(cls_, methods=methods)
562
+
563
+ cls = builder(cls, gatherer=annotation_gatherer, methods=methods, flags={"slotted": False})
564
+
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):
411
580
  """
412
581
  This is a special decorator for making Field subclasses using __slots__.
413
582
  This works by forcing the __init__ method to treat NOTHING as a regular
414
583
  value. This means *all* instance attributes always have defaults.
415
584
 
416
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`
417
588
  :return: Modified subclass
418
589
  """
590
+ if not cls:
591
+ return lambda cls_: fieldclass(cls_, frozen=frozen)
419
592
 
420
- # Fields need a way to call their validate method
421
- # So append it to the code from __init__.
422
- def field_init_func(cls_):
423
- code, globs = init_maker(cls_, null=field_nothing)
424
- code += " self.validate_field()\n"
425
- return code, globs
593
+ field_methods = {_field_init_desc, repr_desc, eq_desc}
426
594
 
427
- field_nothing = _NothingType()
428
- field_init_desc = MethodMaker(
429
- "__init__",
430
- field_init_func,
431
- )
432
- 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})
433
598
 
434
599
  cls = builder(
435
600
  cls,
@@ -6,13 +6,11 @@ _py_type = type # Alias for type where it is used as a name
6
6
  __version__: str
7
7
  INTERNALS_DICT: str
8
8
 
9
- def get_internals(cls) -> dict[str, typing.Any] | None: ...
10
-
11
9
  def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...
12
10
 
13
11
  def get_flags(cls:type) -> dict[str, bool]: ...
14
12
 
15
- def get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
13
+ def _get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
16
14
 
17
15
  class _NothingType:
18
16
  ...
@@ -28,17 +26,24 @@ class MethodMaker:
28
26
  def __repr__(self) -> str: ...
29
27
  def __get__(self, instance, cls) -> Callable: ...
30
28
 
31
- def init_maker(
32
- cls: type,
33
- *,
29
+ def get_init_maker(
34
30
  null: _NothingType = NOTHING,
35
- ) -> tuple[str, dict[str, typing.Any]]: ...
31
+ extra_code: None | list[str] = None
32
+ ) -> Callable[[type], tuple[str, dict[str, typing.Any]]]: ...
33
+
34
+ def init_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
36
35
  def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
37
36
  def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
38
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
+
39
42
  init_desc: MethodMaker
40
43
  repr_desc: MethodMaker
41
44
  eq_desc: MethodMaker
45
+ frozen_setattr_desc: MethodMaker
46
+ frozen_delattr_desc: MethodMaker
42
47
  default_methods: frozenset[MethodMaker]
43
48
 
44
49
  _T = typing.TypeVar("_T")
@@ -90,9 +95,22 @@ class Field:
90
95
  class SlotFields(dict):
91
96
  ...
92
97
 
98
+ def make_slot_gatherer(field_type: type[Field] = Field) -> Callable[[type], dict[str, Field]]: ...
99
+
93
100
  def slot_gatherer(cls: type) -> dict[str, Field]:
94
101
  ...
95
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
+
96
114
  @typing.overload
97
115
  def slotclass(
98
116
  cls: type[_T],
@@ -111,4 +129,24 @@ def slotclass(
111
129
  syntax_check: bool = True
112
130
  ) -> Callable[[type[_T]], type[_T]]: ...
113
131
 
114
- 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
 
@@ -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_internals, slot_gatherer
9
+ builder, fieldclass, get_flags, get_fields, slot_gatherer
10
10
  )
11
11
 
12
12
  # noinspection PyUnresolvedReferences
@@ -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.2.1
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=yZOiy84vo8z0iotOgY1rFwBBngveh1cwoWjcQdRndpw,13747
2
- ducktools/classbuilder/__init__.pyi,sha256=-QHJhPn4EjI4XW4OI74MKNg2tGKJiM3w0ELSOvPstAw,2877
3
- ducktools/classbuilder/prefab.py,sha256=gJJCtTAbQLgIoo79jRWVp09r3u8kJjcqXp4G4FTu9j8,29887
4
- ducktools/classbuilder/prefab.pyi,sha256=GsllqqZ_Wz6i_DenKv-pAbtES3sPanC0g8O7fkNTnvs,3969
5
- ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
6
- ducktools_classbuilder-0.2.1.dist-info/LICENSE.md,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
7
- ducktools_classbuilder-0.2.1.dist-info/METADATA,sha256=kV--mUdR1y0NmszwMobZdnNvigtepG_WF_axupjf6_4,9280
8
- ducktools_classbuilder-0.2.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
9
- ducktools_classbuilder-0.2.1.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
10
- ducktools_classbuilder-0.2.1.dist-info/RECORD,,