ducktools-classbuilder 0.5.0__tar.gz → 0.5.1__tar.gz

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.

Files changed (59) hide show
  1. {ducktools_classbuilder-0.5.0/src/ducktools_classbuilder.egg-info → ducktools_classbuilder-0.5.1}/PKG-INFO +1 -1
  2. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/src/ducktools/classbuilder/__init__.py +33 -3
  3. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/src/ducktools/classbuilder/__init__.pyi +19 -0
  4. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/src/ducktools/classbuilder/prefab.py +40 -9
  5. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/src/ducktools/classbuilder/prefab.pyi +2 -0
  6. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1/src/ducktools_classbuilder.egg-info}/PKG-INFO +1 -1
  7. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/dynamic/test_construction.py +22 -0
  8. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/test_core.py +81 -0
  9. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/LICENSE.md +0 -0
  10. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/MANIFEST.in +0 -0
  11. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/README.md +0 -0
  12. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/docs/Makefile +0 -0
  13. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/docs/api.md +0 -0
  14. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/docs/approach_vs_tool.md +0 -0
  15. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/docs/conf.py +0 -0
  16. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/docs/extension_examples.md +0 -0
  17. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/docs/index.md +0 -0
  18. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/docs/make.bat +0 -0
  19. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/docs/perf/performance_tests.md +0 -0
  20. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/docs/prefab/index.md +0 -0
  21. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/pyproject.toml +0 -0
  22. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/setup.cfg +0 -0
  23. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/src/ducktools/classbuilder/py.typed +0 -0
  24. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/src/ducktools_classbuilder.egg-info/SOURCES.txt +0 -0
  25. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  26. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  27. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  28. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  29. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/dynamic/test_internals.py +0 -0
  30. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  31. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  32. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/dynamic/test_slotted_class.py +0 -0
  33. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/conftest.py +0 -0
  34. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/creation.py +0 -0
  35. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/creation_empty.py +0 -0
  36. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/dunders.py +0 -0
  37. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
  38. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  39. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  40. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  41. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  42. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  43. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/frozen_prefabs.py +0 -0
  44. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  45. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  46. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/inheritance.py +0 -0
  47. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/init_ex.py +0 -0
  48. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/kw_only.py +0 -0
  49. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/examples/repr_func.py +0 -0
  50. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/test_creation.py +0 -0
  51. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/test_dunders.py +0 -0
  52. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/test_frozen.py +0 -0
  53. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/test_funcs.py +0 -0
  54. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/test_hint_syntax.py +0 -0
  55. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/test_inheritance.py +0 -0
  56. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/test_init.py +0 -0
  57. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/test_kw_only.py +0 -0
  58. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/prefab/shared/test_repr.py +0 -0
  59. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.5.1}/tests/test_annotated.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -21,7 +21,7 @@
21
21
  # SOFTWARE.
22
22
  import sys
23
23
 
24
- __version__ = "v0.5.0"
24
+ __version__ = "v0.5.1"
25
25
 
26
26
  # Change this name if you make heavy modifications
27
27
  INTERNALS_DICT = "__classbuilder_internals__"
@@ -359,8 +359,19 @@ class Field:
359
359
  return cls(**argument_dict)
360
360
 
361
361
 
362
+ class GatheredFields:
363
+ __slots__ = ("fields", "modifications")
364
+
365
+ def __init__(self, fields, modifications):
366
+ self.fields = fields
367
+ self.modifications = modifications
368
+
369
+ def __call__(self, cls):
370
+ return self.fields, self.modifications
371
+
372
+
362
373
  # Use the builder to generate __repr__ and __eq__ methods
363
- # and pretend `Field` was a built class all along.
374
+ # for both Field and GatheredFields
364
375
  _field_internal = {
365
376
  "default": Field(default=NOTHING),
366
377
  "default_factory": Field(default=NOTHING),
@@ -368,17 +379,29 @@ _field_internal = {
368
379
  "doc": Field(default=None),
369
380
  }
370
381
 
382
+ _gathered_field_internal = {
383
+ "fields": Field(default=NOTHING),
384
+ "modifications": Field(default=NOTHING),
385
+ }
386
+
371
387
  _field_methods = {repr_maker, eq_maker}
372
388
  if _UNDER_TESTING:
373
389
  _field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
374
390
 
375
391
  builder(
376
392
  Field,
377
- gatherer=lambda cls_: (_field_internal, {}),
393
+ gatherer=GatheredFields(_field_internal, {}),
378
394
  methods=_field_methods,
379
395
  flags={"slotted": True, "kw_only": True},
380
396
  )
381
397
 
398
+ builder(
399
+ GatheredFields,
400
+ gatherer=GatheredFields(_gathered_field_internal, {}),
401
+ methods={repr_maker, eq_maker},
402
+ flags={"slotted": True, "kw_only": False},
403
+ )
404
+
382
405
 
383
406
  # Slot gathering tools
384
407
  # Subclass of dict to be identifiable by isinstance checks
@@ -430,6 +453,13 @@ def make_slot_gatherer(field_type=Field):
430
453
  slot_replacement = {}
431
454
 
432
455
  for k, v in cls_slots.items():
456
+ # Special case __dict__ and __weakref__
457
+ # They should be included in the final `__slots__`
458
+ # But ignored as a value.
459
+ if k in {"__dict__", "__weakref__"}:
460
+ slot_replacement[k] = None
461
+ continue
462
+
433
463
  if isinstance(v, field_type):
434
464
  attrib = v
435
465
  if attrib.type is not NOTHING:
@@ -92,6 +92,25 @@ class Field:
92
92
  def from_field(cls, fld: Field, /, **kwargs: typing.Any) -> Field: ...
93
93
 
94
94
 
95
+ class GatheredFields:
96
+ __slots__ = ("fields", "modifications")
97
+
98
+ fields: dict[str, Field]
99
+ modifications: dict[str, typing.Any]
100
+
101
+ __classbuilder_internals__: dict
102
+
103
+ def __init__(
104
+ self,
105
+ fields: dict[str, Field],
106
+ modifications: dict[str, typing.Any]
107
+ ) -> None: ...
108
+
109
+ def __repr__(self) -> str: ...
110
+ def __eq__(self, other) -> bool: ...
111
+ def __call__(self, cls: type) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
112
+
113
+
95
114
  class SlotFields(dict):
96
115
  ...
97
116
 
@@ -30,7 +30,7 @@ import sys
30
30
 
31
31
  from . import (
32
32
  INTERNALS_DICT, NOTHING,
33
- Field, MethodMaker, SlotFields,
33
+ Field, MethodMaker, SlotFields, GatheredFields,
34
34
  builder, fieldclass, get_flags, get_fields, make_slot_gatherer,
35
35
  frozen_setattr_maker, frozen_delattr_maker, is_classvar,
36
36
  )
@@ -519,6 +519,7 @@ def _make_prefab(
519
519
  frozen=False,
520
520
  dict_method=False,
521
521
  recursive_repr=False,
522
+ gathered_fields=None,
522
523
  ):
523
524
  """
524
525
  Generate boilerplate code for dunder methods in a class.
@@ -535,6 +536,7 @@ def _make_prefab(
535
536
  such as lists)
536
537
  :param dict_method: Include an as_dict method for faster dictionary creation
537
538
  :param recursive_repr: Safely handle repr in case of recursion
539
+ :param gathered_fields: Pre-gathered fields callable, to skip re-collecting attributes
538
540
  :return: class with __ methods defined
539
541
  """
540
542
  cls_dict = cls.__dict__
@@ -546,12 +548,16 @@ def _make_prefab(
546
548
  )
547
549
 
548
550
  slots = cls_dict.get("__slots__")
549
- if isinstance(slots, SlotFields):
550
- gatherer = slot_prefab_gatherer
551
- slotted = True
551
+ if gathered_fields is None:
552
+ if isinstance(slots, SlotFields):
553
+ gatherer = slot_prefab_gatherer
554
+ slotted = True
555
+ else:
556
+ gatherer = attribute_gatherer
557
+ slotted = False
552
558
  else:
553
- gatherer = attribute_gatherer
554
- slotted = False
559
+ gatherer = gathered_fields
560
+ slotted = False if slots is None else True
555
561
 
556
562
  methods = set()
557
563
 
@@ -770,6 +776,7 @@ def build_prefab(
770
776
  frozen=False,
771
777
  dict_method=False,
772
778
  recursive_repr=False,
779
+ slots=False,
773
780
  ):
774
781
  """
775
782
  Dynamically construct a (dynamic) prefab.
@@ -790,12 +797,35 @@ def build_prefab(
790
797
  (This does not prevent the modification of mutable attributes such as lists)
791
798
  :param dict_method: Include an as_dict method for faster dictionary creation
792
799
  :param recursive_repr: Safely handle repr in case of recursion
800
+ :param slots: Make the resulting class slotted
793
801
  :return: class with __ methods defined
794
802
  """
795
- class_dict = {} if class_dict is None else class_dict
796
- cls = type(class_name, bases, class_dict)
803
+ class_dict = {} if class_dict is None else class_dict.copy()
804
+
805
+ class_annotations = {}
806
+ class_slots = {}
807
+ fields = {}
808
+
797
809
  for name, attrib in attributes:
798
- setattr(cls, name, attrib)
810
+ if isinstance(attrib, Attribute):
811
+ fields[name] = attrib
812
+ elif isinstance(attrib, Field):
813
+ fields[name] = Attribute.from_field(attrib)
814
+ else:
815
+ fields[name] = Attribute(default=attrib)
816
+
817
+ if attrib.type is not NOTHING:
818
+ class_annotations[name] = attrib.type
819
+
820
+ class_slots[name] = attrib.doc
821
+
822
+ if slots:
823
+ class_dict["__slots__"] = class_slots
824
+
825
+ class_dict["__annotations__"] = class_annotations
826
+ cls = type(class_name, bases, class_dict)
827
+
828
+ gathered_fields = GatheredFields(fields, {})
799
829
 
800
830
  cls = _make_prefab(
801
831
  cls,
@@ -808,6 +838,7 @@ def build_prefab(
808
838
  frozen=frozen,
809
839
  dict_method=dict_method,
810
840
  recursive_repr=recursive_repr,
841
+ gathered_fields=gathered_fields,
811
842
  )
812
843
 
813
844
  return cls
@@ -109,6 +109,7 @@ def _make_prefab(
109
109
  frozen: bool = False,
110
110
  dict_method: bool = False,
111
111
  recursive_repr: bool = False,
112
+ gathered_fields: Callable[[type], tuple[dict[str, Attribute], dict[str, typing.Any]]] | None = None,
112
113
  ) -> type: ...
113
114
 
114
115
  _T = typing.TypeVar("_T")
@@ -146,6 +147,7 @@ def build_prefab(
146
147
  frozen: bool = False,
147
148
  dict_method: bool = False,
148
149
  recursive_repr: bool = False,
150
+ slots: bool = False,
149
151
  ) -> type: ...
150
152
 
151
153
  def is_prefab(o: typing.Any) -> bool: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -91,3 +91,25 @@ def test_kwonly_class():
91
91
  a: int = 0
92
92
  b: int = attribute(kw_only=True)
93
93
  c: int = attribute() # kw_only should be ignored
94
+
95
+
96
+ def test_build_slotted():
97
+ SlottedClass = build_prefab(
98
+ "SlottedClass",
99
+ [
100
+ ("x", attribute(doc="x co-ordinate", type=float)),
101
+ ("y", attribute(default=0, doc="y co-ordinate", type=float))
102
+ ],
103
+ slots=True,
104
+ )
105
+
106
+ inst = SlottedClass(1)
107
+ assert inst.x == 1
108
+ assert inst.y == 0
109
+ assert SlottedClass.__slots__ == {'x': "x co-ordinate", 'y': "y co-ordinate"}
110
+
111
+ assert SlottedClass.__annotations__ == {'x': float, 'y': float}
112
+
113
+ # Test slots are functioning
114
+ with pytest.raises(AttributeError):
115
+ inst.z = 0
@@ -14,6 +14,7 @@ from ducktools.classbuilder import (
14
14
  slot_gatherer,
15
15
  slotclass,
16
16
  fieldclass,
17
+ GatheredFields,
17
18
  )
18
19
 
19
20
 
@@ -299,6 +300,58 @@ def test_slotclass_norepr_noeq():
299
300
  assert "__eq__" not in SlotClass.__dict__
300
301
 
301
302
 
303
+ def test_slotclass_weakref():
304
+ import weakref
305
+
306
+ @slotclass
307
+ class WeakrefClass:
308
+ __slots__ = SlotFields(
309
+ a=1,
310
+ b=2,
311
+ __weakref__=None,
312
+ )
313
+
314
+ flds = get_fields(WeakrefClass)
315
+ assert 'a' in flds
316
+ assert 'b' in flds
317
+ assert '__weakref__' not in flds
318
+
319
+ slots = WeakrefClass.__slots__
320
+ assert 'a' in slots
321
+ assert 'b' in slots
322
+ assert '__weakref__' in slots
323
+
324
+ # Test weakrefs can be created
325
+ inst = WeakrefClass()
326
+ ref = weakref.ref(inst)
327
+ assert ref == inst.__weakref__
328
+
329
+
330
+ def test_slotclass_dict():
331
+ @slotclass
332
+ class DictClass:
333
+ __slots__ = SlotFields(
334
+ a=1,
335
+ b=2,
336
+ __dict__=None,
337
+ )
338
+
339
+ flds = get_fields(DictClass)
340
+ assert 'a' in flds
341
+ assert 'b' in flds
342
+ assert '__dict__' not in flds
343
+
344
+ slots = DictClass.__slots__
345
+ assert 'a' in slots
346
+ assert 'b' in slots
347
+ assert '__dict__' in slots
348
+
349
+ # Test if __dict__ is included new values can be added
350
+ inst = DictClass()
351
+ inst.c = 42
352
+ assert inst.__dict__ == {"c": 42}
353
+
354
+
302
355
  def test_fieldclass():
303
356
  @fieldclass
304
357
  class NewField(Field):
@@ -374,3 +427,31 @@ def test_builder_noclass():
374
427
  assert x.a == 12
375
428
  assert x.b == 2
376
429
  assert x.c == []
430
+
431
+
432
+ def test_gatheredfields():
433
+ fields = {"x": Field(default=1)}
434
+ modifications = {"x": NOTHING}
435
+
436
+ alt_fields = {"x": Field(default=1), "y": Field(default=2)}
437
+
438
+ flds = GatheredFields(fields, modifications)
439
+ flds_2 = GatheredFields(fields, modifications)
440
+ flds_3 = GatheredFields(alt_fields, modifications)
441
+
442
+ class Ex:
443
+ pass
444
+
445
+ assert flds(Ex) == (fields, modifications)
446
+
447
+ assert flds == flds_2
448
+ assert flds != flds_3
449
+ assert flds != object()
450
+
451
+ assert repr(flds).endswith(
452
+ "GatheredFields("
453
+ "fields={'x': Field(default=1, default_factory=<NOTHING OBJECT>, type=<NOTHING OBJECT>, doc=None)}, "
454
+ "modifications={'x': <NOTHING OBJECT>}"
455
+ ")"
456
+ )
457
+