ducktools-classbuilder 0.1.1__tar.gz → 0.2.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 (57) hide show
  1. {ducktools_classbuilder-0.1.1/src/ducktools_classbuilder.egg-info → ducktools_classbuilder-0.2.1}/PKG-INFO +1 -1
  2. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/api.md +1 -1
  3. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/extension_examples.md +219 -33
  4. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/__init__.py +34 -11
  5. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/__init__.pyi +7 -4
  6. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/prefab.py +35 -24
  7. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/prefab.pyi +5 -3
  8. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1/src/ducktools_classbuilder.egg-info}/PKG-INFO +1 -1
  9. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_funcs.py +3 -3
  10. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/LICENSE.md +0 -0
  11. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/MANIFEST.in +0 -0
  12. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/README.md +0 -0
  13. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/Makefile +0 -0
  14. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/approach_vs_tool.md +0 -0
  15. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/conf.py +0 -0
  16. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/index.md +0 -0
  17. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/make.bat +0 -0
  18. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/perf/performance_tests.md +0 -0
  19. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/prefab/index.md +0 -0
  20. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/pyproject.toml +0 -0
  21. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/setup.cfg +0 -0
  22. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/py.typed +0 -0
  23. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools_classbuilder.egg-info/SOURCES.txt +0 -0
  24. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  25. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  26. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  27. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  28. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_construction.py +0 -0
  29. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_internals.py +0 -0
  30. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  31. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  32. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_slotted_class.py +0 -0
  33. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/conftest.py +0 -0
  34. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/creation.py +0 -0
  35. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/creation_empty.py +0 -0
  36. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/dunders.py +0 -0
  37. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
  38. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  39. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  40. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  41. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  42. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  43. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/frozen_prefabs.py +0 -0
  44. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  45. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  46. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/inheritance.py +0 -0
  47. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/init_ex.py +0 -0
  48. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/kw_only.py +0 -0
  49. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/repr_func.py +0 -0
  50. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_creation.py +0 -0
  51. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_dunders.py +0 -0
  52. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_frozen.py +0 -0
  53. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_hint_syntax.py +0 -0
  54. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_inheritance.py +0 -0
  55. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_init.py +0 -0
  56. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_kw_only.py +0 -0
  57. {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_repr.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -25,7 +25,7 @@
25
25
  ```
26
26
 
27
27
  ```{eval-rst}
28
- .. autofunction:: ducktools.classbuilder::get_internals
28
+ .. autofunction:: ducktools.classbuilder::get_flags
29
29
  ```
30
30
 
31
31
  ```{eval-rst}
@@ -33,16 +33,31 @@ create `__init__` and other magic methods are added to the class.
33
33
  This function is the core class generator which takes your decorated class and
34
34
  analyses and collects valid fields and then attaches the method makers.
35
35
 
36
- The field information is stored in the `INTERNALS_DICT` attribute and can be
37
- accessed using the `get_internals` function provided. This returns a dictionary
38
- with 2 keys: `local_fields` and `fields`.
36
+ The field information is stored in the `INTERNALS_DICT` attribute which should generally
37
+ not need to be accessed directly. `get_fields` and `get_flags` functions are to be
38
+ used to access the important keys.
39
39
 
40
- `"local_fields"` contains the field information obtained from **this class only**.
40
+ `get_fields(cls)` will return the resolved information obtained from this class and subclasses.
41
41
 
42
- `"fields"` contains the resolved information obtained from this class and subclasses.
43
- This can be obtained directly using the `get_fields` function.
42
+ `get_fields(cls, local=True)` will return the field information obtained from **this class only**.
44
43
 
45
- Now let's look at what the two keyword arguments need to be.
44
+ Now let's look at what the keyword arguments to `builder` need to be.
45
+
46
+ #### Flags ####
47
+
48
+ Flags are information that defines how the entire class should be generated, for use by
49
+ method generators when operating on the class.
50
+
51
+ The default makers in `ducktools.classbuilder` make use of one flag - `"kw_only"` -
52
+ which indicates that a class `__init__` function should only take keyword arguments.
53
+
54
+ Prefabs also make use of a `"slotted"` flag to indicate if the class has been generated
55
+ with `__slots__` (checking for the existence of `__slots__` could find that a user has
56
+ manually placed slots in the class).
57
+
58
+ Flags are set using a dictionary with these keys and boolean values, for example:
59
+
60
+ `cls = builder(cls, gatherer=..., methods=..., flags={"kw_only": True, "slotted": True})`
46
61
 
47
62
  #### Gatherers ####
48
63
 
@@ -672,62 +687,233 @@ if __name__ == "__main__":
672
687
  print(H2G2.the_author)
673
688
  ```
674
689
 
675
- #### No attributes! Only Annotations! ####
690
+ #### What about using annotations instead of `Field(init=False, ...)` ####
676
691
 
677
- If you don't like your code to run quickly, but you do love type annotations.
678
-
679
- This does everything using `Annotated` and so requires Python 3.10 for both
680
- this and get_annotations.
692
+ This seems to be a feature people keep requesting for `dataclasses`.
693
+ This is also doable.
681
694
 
682
695
  ```python
683
696
  import inspect
697
+ from pprint import pp
684
698
  from typing import Annotated, Any, ClassVar, get_origin
685
699
 
686
- from ducktools.classbuilder import builder, default_methods, Field
700
+ from ducktools.classbuilder import (
701
+ builder,
702
+ fieldclass,
703
+ get_fields,
704
+ get_flags,
705
+ Field,
706
+ MethodMaker,
707
+ SlotFields,
708
+ NOTHING,
709
+ )
710
+
711
+
712
+ # New equivalent to dataclasses "Field", these still need to be created
713
+ # in order to generate the magic methods correctly.
714
+ @fieldclass
715
+ class AnnoField(Field):
716
+ __slots__ = SlotFields(
717
+ init=True,
718
+ repr=True,
719
+ compare=True,
720
+ kw_only=False,
721
+ )
722
+
723
+
724
+ # Modifying objects
725
+ class FieldModifier:
726
+ __slots__ = ("modifiers", )
727
+ modifiers: dict[str, Any]
728
+
729
+ def __init__(self, **modifiers):
730
+ self.modifiers = modifiers
731
+
732
+ def __repr__(self):
733
+ mod_args = ", ".join(f"{k}={v!r}" for k, v in self.modifiers.items())
734
+ return (
735
+ f"{type(self).__name__}({mod_args})"
736
+ )
737
+
738
+ def __eq__(self, other):
739
+ if self.__class__ == other.__class__:
740
+ return self.modifiers == other.modifiers
741
+ return NotImplemented
742
+
743
+
744
+ KW_ONLY = FieldModifier(kw_only=True)
745
+ NO_INIT = FieldModifier(init=False)
746
+ NO_REPR = FieldModifier(repr=False)
747
+ NO_COMPARE = FieldModifier(compare=False)
748
+ IGNORE_ALL = FieldModifier(init=False, repr=False, compare=False)
687
749
 
688
750
 
689
751
  def annotated_gatherer(cls: type) -> dict[str, Any]:
752
+ # String annotations *MUST* be evaluated for this to work
753
+ # dataclasses currently does not require this
690
754
  cls_annotations = inspect.get_annotations(cls, eval_str=True)
691
755
  cls_fields = {}
692
756
 
693
757
  for key, anno in cls_annotations.items():
694
- # Is there another way to do this?
758
+ modifiers = {}
759
+ typ = NOTHING
760
+
695
761
  if get_origin(anno) is Annotated:
696
762
  typ = anno.__args__[0]
697
763
  meta = anno.__metadata__
698
764
  for v in meta:
699
- if isinstance(v, Field):
700
- fld = Field.from_field(v, type=typ)
701
- break
702
- else:
703
- fld = Field(type=typ)
704
- elif anno is ClassVar or get_origin(anno) is ClassVar:
705
- fld = None
706
- else:
765
+ if isinstance(v, FieldModifier):
766
+ modifiers.update(v.modifiers)
767
+
768
+ elif not (anno is ClassVar or get_origin(anno) is ClassVar):
707
769
  typ = anno
708
- fld = Field(type=typ)
709
770
 
710
- if fld:
711
- cls_fields[key] = fld
771
+ if typ is not NOTHING:
712
772
  if key in cls.__dict__ and "__slots__" not in cls.__dict__:
713
- raise AttributeError("No attributes! Only Annotations!")
773
+ val = cls.__dict__[key]
774
+ if isinstance(val, Field):
775
+ fld = AnnoField.from_field(val, type=typ, **modifiers)
776
+ else:
777
+ fld = AnnoField(default=val, type=typ, **modifiers)
778
+ else:
779
+ fld = AnnoField(type=typ, **modifiers)
780
+
781
+ cls_fields[key] = fld
714
782
 
715
783
  return cls_fields
716
784
 
717
785
 
718
- def annotationsclass(cls):
719
- return builder(cls, gatherer=annotated_gatherer, methods=default_methods)
786
+ def init_maker(cls):
787
+
788
+ fields = get_fields(cls)
789
+ flags = get_flags(cls)
790
+
791
+ arglist = []
792
+ kw_only_arglist = []
793
+
794
+ assignments = []
795
+ globs = {}
796
+
797
+ # Whole class kw_only
798
+ kw_only = flags.get("kw_only", False)
799
+
800
+ for k, v in fields.items():
801
+ if getattr(v, "init", True):
802
+ if v.default is not NOTHING:
803
+ globs[f"_{k}_default"] = v.default
804
+ arg = f"{k}=_{k}_default"
805
+ assignment = f"self.{k} = {k}"
806
+ elif v.default_factory is not NOTHING:
807
+ globs[f"_{k}_factory"] = v.default_factory
808
+ arg = f"{k}=None"
809
+ assignment = f"self.{k} = _{k}_factory() if {k} is None else {k}"
810
+ else:
811
+ arg = f"{k}"
812
+ assignment = f"self.{k} = {k}"
813
+
814
+ if getattr(v, "kw_only", False) or kw_only:
815
+ kw_only_arglist.append(arg)
816
+ else:
817
+ arglist.append(arg)
818
+
819
+ assignments.append(assignment)
820
+ else:
821
+ if v.default is not NOTHING:
822
+ globs[f"_{k}_default"] = v.default
823
+ assignment = f"self.{k} = _{k}_default"
824
+ assignments.append(assignment)
825
+ elif v.default_factory is not NOTHING:
826
+ globs[f"_{k}_factory"] = v.default_factory
827
+ assignment = f"self.{k} = _{k}_factory()"
828
+ assignments.append(assignment)
829
+
830
+ if kw_only_arglist:
831
+ arglist.append("*")
832
+ arglist.extend(kw_only_arglist)
833
+
834
+ args = ", ".join(arglist)
835
+ assigns = "\n ".join(assignments)
836
+ code = f"def __init__(self, {args}):\n" f" {assigns}\n"
837
+
838
+ return code, globs
839
+
840
+
841
+ def repr_maker(cls):
842
+ fields = get_fields(cls)
843
+ content = ", ".join(
844
+ f"{name}={{self.{name}!r}}"
845
+ for name, fld in fields.items()
846
+ if getattr(fld, "repr", True)
847
+ )
848
+ code = (
849
+ f"def __repr__(self):\n"
850
+ f" return f'{{type(self).__qualname__}}({content})'\n"
851
+ )
852
+ globs = {}
853
+ return code, globs
854
+
855
+
856
+ def eq_maker(cls):
857
+ class_comparison = "self.__class__ is other.__class__"
858
+ field_names = [
859
+ name
860
+ for name, fld in get_fields(cls).items()
861
+ if getattr(fld, "compare", True)
862
+ ]
863
+
864
+ if field_names:
865
+ selfvals = ",".join(f"self.{name}" for name in field_names)
866
+ othervals = ",".join(f"other.{name}" for name in field_names)
867
+ instance_comparison = f"({selfvals},) == ({othervals},)"
868
+ else:
869
+ instance_comparison = "True"
870
+
871
+ code = (
872
+ f"def __eq__(self, other):\n"
873
+ f" return {instance_comparison} if {class_comparison} else NotImplemented\n"
874
+ )
875
+ globs = {}
876
+
877
+ return code, globs
878
+
879
+
880
+ init_method = MethodMaker("__init__", init_maker)
881
+ repr_method = MethodMaker("__repr__", repr_maker)
882
+ eq_method = MethodMaker("__eq__", eq_maker)
883
+
884
+ methods = {init_method, repr_method, eq_method}
885
+
886
+
887
+ def annotationsclass(cls=None, *, kw_only=False):
888
+ if not cls:
889
+ return lambda cls_: annotationsclass(cls_, kw_only=kw_only)
890
+
891
+ return builder(
892
+ cls,
893
+ gatherer=annotated_gatherer,
894
+ methods=methods,
895
+ flags={"slotted": False, "kw_only": kw_only}
896
+ )
720
897
 
721
898
 
722
899
  @annotationsclass
723
900
  class X:
724
901
  x: str
725
902
  y: ClassVar[str] = "This is okay"
726
- a: Annotated[int, Field(default=1)]
727
- b: Annotated[str, Field(default="example")]
728
- c: Annotated[list[str], Field(default_factory=list)]
903
+ a: Annotated[int, NO_INIT] = "Not In __init__ signature"
904
+ b: Annotated[str, NO_REPR] = "Not In Repr"
905
+ c: Annotated[list[str], NO_COMPARE] = AnnoField(default_factory=list)
906
+ d: Annotated[str, IGNORE_ALL] = "Not Anywhere"
907
+ e: Annotated[str, KW_ONLY, NO_COMPARE]
908
+
909
+
910
+ ex = X("Value of x", e="Value of e")
729
911
 
912
+ print(ex, "\n")
730
913
 
731
- print(X("Testing"))
732
- print(X.y)
914
+ pp(get_fields(X))
915
+ print("\nSource:")
916
+ print(init_maker(X)[0])
917
+ print(eq_maker(X)[0])
918
+ print(repr_maker(X)[0])
733
919
  ```
@@ -19,7 +19,7 @@
19
19
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
- __version__ = "v0.1.1"
22
+ __version__ = "v0.2.1"
23
23
 
24
24
  # Change this name if you make heavy modifications
25
25
  INTERNALS_DICT = "__classbuilder_internals__"
@@ -34,6 +34,9 @@ def get_internals(cls):
34
34
  and 'local_fields' attributes this will always
35
35
  evaluate as 'truthy' if this is a generated class.
36
36
 
37
+ Generally you should use the helper get_flags and
38
+ get_fields methods.
39
+
37
40
  Usage:
38
41
  if internals := get_internals(cls):
39
42
  ...
@@ -44,15 +47,28 @@ def get_internals(cls):
44
47
  return getattr(cls, INTERNALS_DICT, None)
45
48
 
46
49
 
47
- def get_fields(cls):
50
+ def get_fields(cls, *, local=False):
48
51
  """
49
52
  Utility function to gather the fields dictionary
50
53
  from the class internals.
51
54
 
52
55
  :param cls: generated class
56
+ :param local: get only fields that were not inherited
53
57
  :return: dictionary of keys and Field attribute info
54
58
  """
55
- return getattr(cls, INTERNALS_DICT)["fields"]
59
+ key = "local_fields" if local else "fields"
60
+ return getattr(cls, INTERNALS_DICT)[key]
61
+
62
+
63
+ def get_flags(cls):
64
+ """
65
+ Utility function to gather the flags dictionary
66
+ from the class internals.
67
+
68
+ :param cls: generated class
69
+ :return: dictionary of keys and flag values
70
+ """
71
+ return getattr(cls, INTERNALS_DICT)["flags"]
56
72
 
57
73
 
58
74
  def get_inst_fields(inst):
@@ -106,14 +122,15 @@ class MethodMaker:
106
122
  return method.__get__(instance, cls)
107
123
 
108
124
 
109
- def init_maker(cls, *, null=NOTHING, kw_only=False):
125
+ def init_maker(cls, *, null=NOTHING):
110
126
  fields = get_fields(cls)
127
+ flags = get_flags(cls)
111
128
 
112
129
  arglist = []
113
130
  assignments = []
114
131
  globs = {}
115
132
 
116
- if kw_only:
133
+ if flags.get("kw_only", False):
117
134
  arglist.append("*")
118
135
 
119
136
  for k, v in fields.items():
@@ -180,7 +197,7 @@ eq_desc = MethodMaker("__eq__", eq_maker)
180
197
  default_methods = frozenset({init_desc, repr_desc, eq_desc})
181
198
 
182
199
 
183
- def builder(cls=None, /, *, gatherer, methods):
200
+ def builder(cls=None, /, *, gatherer, methods, flags=None):
184
201
  """
185
202
  The main builder for class generation
186
203
 
@@ -189,6 +206,8 @@ def builder(cls=None, /, *, gatherer, methods):
189
206
  :type gatherer: Callable[[type], dict[str, Field]]
190
207
  :param methods: MethodMakers to add to the class
191
208
  :type methods: set[MethodMaker]
209
+ :param flags: additional flags to store in the internals dictionary
210
+ for use by method generators.
192
211
  :return: The modified class (the class itself is modified, but this is expected).
193
212
  """
194
213
  # Handle `None` to make wrapping with a decorator easier.
@@ -197,6 +216,7 @@ def builder(cls=None, /, *, gatherer, methods):
197
216
  cls_,
198
217
  gatherer=gatherer,
199
218
  methods=methods,
219
+ flags=flags,
200
220
  )
201
221
 
202
222
  internals = {}
@@ -212,11 +232,12 @@ def builder(cls=None, /, *, gatherer, methods):
212
232
  fields = {}
213
233
  for c in reversed(mro):
214
234
  try:
215
- fields.update(get_internals(c)["local_fields"])
235
+ fields.update(get_fields(c, local=True))
216
236
  except AttributeError:
217
237
  pass
218
238
 
219
239
  internals["fields"] = fields
240
+ internals["flags"] = flags if flags is not None else {}
220
241
 
221
242
  # Assign all of the method generators
222
243
  for method in methods:
@@ -296,7 +317,8 @@ _field_internal = {
296
317
  builder(
297
318
  Field,
298
319
  gatherer=lambda cls_: _field_internal,
299
- methods=frozenset({repr_desc, eq_desc})
320
+ methods=frozenset({repr_desc, eq_desc}),
321
+ flags={"slotted": True, "kw_only": True},
300
322
  )
301
323
 
302
324
 
@@ -368,7 +390,7 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
368
390
  if not cls:
369
391
  return lambda cls_: slotclass(cls_, methods=methods, syntax_check=syntax_check)
370
392
 
371
- cls = builder(cls, gatherer=slot_gatherer, methods=methods)
393
+ cls = builder(cls, gatherer=slot_gatherer, methods=methods, flags={"slotted": True})
372
394
 
373
395
  if syntax_check:
374
396
  fields = get_fields(cls)
@@ -398,7 +420,7 @@ def fieldclass(cls):
398
420
  # Fields need a way to call their validate method
399
421
  # So append it to the code from __init__.
400
422
  def field_init_func(cls_):
401
- code, globs = init_maker(cls_, null=field_nothing, kw_only=True)
423
+ code, globs = init_maker(cls_, null=field_nothing)
402
424
  code += " self.validate_field()\n"
403
425
  return code, globs
404
426
 
@@ -412,7 +434,8 @@ def fieldclass(cls):
412
434
  cls = builder(
413
435
  cls,
414
436
  gatherer=slot_gatherer,
415
- methods=field_methods
437
+ methods=field_methods,
438
+ flags={"slotted": True, "kw_only": True}
416
439
  )
417
440
 
418
441
  return cls
@@ -8,7 +8,9 @@ INTERNALS_DICT: str
8
8
 
9
9
  def get_internals(cls) -> dict[str, typing.Any] | None: ...
10
10
 
11
- def get_fields(cls: type) -> dict[str, Field]: ...
11
+ def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...
12
+
13
+ def get_flags(cls:type) -> dict[str, bool]: ...
12
14
 
13
15
  def get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
14
16
 
@@ -30,7 +32,6 @@ def init_maker(
30
32
  cls: type,
31
33
  *,
32
34
  null: _NothingType = NOTHING,
33
- kw_only: bool = False
34
35
  ) -> tuple[str, dict[str, typing.Any]]: ...
35
36
  def repr_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
36
37
  def eq_maker(cls: type) -> tuple[str, dict[str, typing.Any]]: ...
@@ -48,7 +49,8 @@ def builder(
48
49
  /,
49
50
  *,
50
51
  gatherer: Callable[[type], dict[str, Field]],
51
- methods: frozenset[MethodMaker] | set[MethodMaker]
52
+ methods: frozenset[MethodMaker] | set[MethodMaker],
53
+ flags: dict[str, bool] | None = None,
52
54
  ) -> type[_T]: ...
53
55
 
54
56
  @typing.overload
@@ -57,7 +59,8 @@ def builder(
57
59
  /,
58
60
  *,
59
61
  gatherer: Callable[[type], dict[str, Field]],
60
- methods: frozenset[MethodMaker] | set[MethodMaker]
62
+ methods: frozenset[MethodMaker] | set[MethodMaker],
63
+ flags: dict[str, bool] | None = None,
61
64
  ) -> Callable[[type[_T]], type[_T]]: ...
62
65
 
63
66
 
@@ -31,7 +31,7 @@ import sys
31
31
  from . import (
32
32
  INTERNALS_DICT, NOTHING,
33
33
  Field, MethodMaker, SlotFields,
34
- builder, fieldclass, get_internals, slot_gatherer
34
+ builder, fieldclass, get_flags, get_fields, slot_gatherer
35
35
  )
36
36
 
37
37
  PREFAB_FIELDS = "PREFAB_FIELDS"
@@ -84,10 +84,11 @@ def get_attributes(cls):
84
84
  def get_init_maker(*, init_name="__init__"):
85
85
  def __init__(cls: "type") -> "tuple[str, dict]":
86
86
  globs = {}
87
- internals = get_internals(cls)
88
87
  # Get the internals dictionary and prepare attributes
89
- attributes = internals["fields"]
90
- kw_only = internals["kw_only"]
88
+ attributes = get_attributes(cls)
89
+ flags = get_flags(cls)
90
+
91
+ kw_only = flags.get("kw_only", False)
91
92
 
92
93
  # Handle pre/post init first - post_init can change types for __init__
93
94
  # Get pre and post init arguments
@@ -342,14 +343,16 @@ def get_iter_maker():
342
343
  def get_frozen_setattr_maker():
343
344
  def __setattr__(cls: "type") -> "tuple[str, dict]":
344
345
  globs = {}
345
- internals = get_internals(cls)
346
- field_names = internals["fields"].keys()
346
+ attributes = get_attributes(cls)
347
+ flags = get_flags(cls)
347
348
 
348
349
  # Make the fields set literal
349
- fields_delimited = ", ".join(f"{field!r}" for field in field_names)
350
+ fields_delimited = ", ".join(f"{field!r}" for field in attributes)
350
351
  field_set = f"{{ {fields_delimited} }}"
351
352
 
352
- if internals["slotted"]:
353
+ # Better to be safe and use the method that works in both cases
354
+ # if somehow slotted has not been set.
355
+ if flags.get("slotted", True):
353
356
  globs["__prefab_setattr_func"] = object.__setattr__
354
357
  setattr_method = "__prefab_setattr_func(self, name, value)"
355
358
  else:
@@ -395,7 +398,7 @@ def get_asdict_maker():
395
398
  vals = ", ".join(
396
399
  f"'{name}': self.{name}"
397
400
  for name, attrib in fields.items()
398
- if attrib.in_dict and not attrib.exclude_field
401
+ if attrib.serialize and not attrib.exclude_field
399
402
  )
400
403
  out_dict = f"{{{vals}}}"
401
404
  code = f"def as_dict(self): return {out_dict}"
@@ -425,7 +428,7 @@ class Attribute(Field):
425
428
  compare=True,
426
429
  iter=True,
427
430
  kw_only=False,
428
- in_dict=True,
431
+ serialize=True,
429
432
  exclude_field=False,
430
433
  )
431
434
 
@@ -447,7 +450,7 @@ def attribute(
447
450
  compare=True,
448
451
  iter=True,
449
452
  kw_only=False,
450
- in_dict=True,
453
+ serialize=True,
451
454
  exclude_field=False,
452
455
  doc=None,
453
456
  type=NOTHING,
@@ -463,7 +466,7 @@ def attribute(
463
466
  :param compare: Include this attribute in the class __eq__
464
467
  :param iter: Include this attribute in the class __iter__ if generated
465
468
  :param kw_only: Make this argument keyword only in init
466
- :param in_dict: Include this attribute in methods that serialise to dict
469
+ :param serialize: Include this attribute in methods that serialize to dict
467
470
  :param exclude_field: Exclude this field from all magic method generation
468
471
  apart from __init__ signature
469
472
  and do not include it in PREFAB_FIELDS
@@ -481,13 +484,21 @@ def attribute(
481
484
  compare=compare,
482
485
  iter=iter,
483
486
  kw_only=kw_only,
484
- in_dict=in_dict,
487
+ serialize=serialize,
485
488
  exclude_field=exclude_field,
486
489
  doc=doc,
487
490
  type=type,
488
491
  )
489
492
 
490
493
 
494
+ def slot_prefab_gatherer(cls):
495
+ # For prefabs it's easier if everything is an attribute
496
+ return {
497
+ name: Attribute.from_field(fld)
498
+ for name, fld in slot_gatherer(cls).items()
499
+ }
500
+
501
+
491
502
  # Gatherer for classes built on attributes or annotations
492
503
  def attribute_gatherer(cls):
493
504
  cls_annotations = cls.__dict__.get("__annotations__", {})
@@ -599,7 +610,7 @@ def _make_prefab(
599
610
 
600
611
  slots = cls_dict.get("__slots__")
601
612
  if isinstance(slots, SlotFields):
602
- gatherer = slot_gatherer
613
+ gatherer = slot_prefab_gatherer
603
614
  slotted = True
604
615
  else:
605
616
  gatherer = attribute_gatherer
@@ -627,18 +638,20 @@ def _make_prefab(
627
638
  if dict_method:
628
639
  methods.add(asdict_desc)
629
640
 
641
+ flags = {
642
+ "kw_only": kw_only,
643
+ "slotted": slotted,
644
+ }
645
+
630
646
  cls = builder(
631
647
  cls,
632
648
  gatherer=gatherer,
633
649
  methods=methods,
650
+ flags=flags,
634
651
  )
635
652
 
636
- # Add fields not covered by builder
637
- internals = get_internals(cls)
638
- internals["slotted"] = slotted
639
- internals["kw_only"] = kw_only
640
- fields = internals["fields"]
641
- local_fields = internals["local_fields"]
653
+ # Get fields now the class has been built
654
+ fields = get_fields(cls)
642
655
 
643
656
  # Check pre_init and post_init functions if they exist
644
657
  try:
@@ -710,8 +723,6 @@ def _make_prefab(
710
723
  if not isinstance(attrib, Attribute):
711
724
  attrib = Attribute.from_field(attrib)
712
725
  fields[name] = attrib
713
- if name in local_fields:
714
- local_fields[name] = attrib
715
726
 
716
727
  # Excluded fields *MUST* be forwarded to post_init
717
728
  if attrib.exclude_field:
@@ -895,7 +906,7 @@ def is_prefab_instance(o):
895
906
 
896
907
  def as_dict(o):
897
908
  """
898
- Get the valid fields from a prefab respecting the in_dict
909
+ Get the valid fields from a prefab respecting the serialize
899
910
  values of attributes
900
911
 
901
912
  :param o: instance of a prefab class
@@ -916,5 +927,5 @@ def as_dict(o):
916
927
  return {
917
928
  name: getattr(o, name)
918
929
  for name, attrib in flds.items()
919
- if attrib.in_dict and not attrib.exclude_field
930
+ if attrib.serialize and not attrib.exclude_field
920
931
  }
@@ -63,7 +63,7 @@ class Attribute(Field):
63
63
  compare: bool
64
64
  iter: bool
65
65
  kw_only: bool
66
- in_dict: bool
66
+ serialize: bool
67
67
  exclude_field: bool
68
68
 
69
69
  def __init__(
@@ -78,7 +78,7 @@ class Attribute(Field):
78
78
  compare: bool = True,
79
79
  iter: bool = True,
80
80
  kw_only: bool = False,
81
- in_dict: bool = True,
81
+ serialize: bool = True,
82
82
  exclude_field: bool = False,
83
83
  ) -> None: ...
84
84
 
@@ -97,10 +97,12 @@ def attribute(
97
97
  compare: bool = True,
98
98
  iter: bool = True,
99
99
  kw_only: bool = False,
100
- in_dict: bool = True,
100
+ serialize: bool = True,
101
101
  exclude_field: bool = False,
102
102
  ) -> Attribute: ...
103
103
 
104
+ def slot_prefab_gatherer(cls: type) -> dict[str, Attribute]: ...
105
+
104
106
  def attribute_gatherer(cls: type) -> dict[str, Attribute]: ...
105
107
 
106
108
  def _make_prefab(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -45,18 +45,18 @@ def test_as_dict_excludes():
45
45
  @prefab
46
46
  class ExcludesUncached:
47
47
  name: str
48
- password: str = attribute(in_dict=False)
48
+ password: str = attribute(serialize=False)
49
49
 
50
50
  @prefab(dict_method=True)
51
51
  class ExcludesCached:
52
52
  name: str
53
- password: str = attribute(in_dict=False)
53
+ password: str = attribute(serialize=False)
54
54
 
55
55
  @prefab(dict_method=True)
56
56
  class ExcludesSlots:
57
57
  __slots__ = SlotFields(
58
58
  name=attribute(type=str),
59
- password=attribute(in_dict=False, type=str)
59
+ password=attribute(serialize=False, type=str)
60
60
  )
61
61
 
62
62
  @prefab(dict_method=True)