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

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

Potentially problematic release.


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

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