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.
- {ducktools_classbuilder-0.1.1/src/ducktools_classbuilder.egg-info → ducktools_classbuilder-0.2.1}/PKG-INFO +1 -1
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/api.md +1 -1
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/extension_examples.md +219 -33
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/__init__.py +34 -11
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/__init__.pyi +7 -4
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/prefab.py +35 -24
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/prefab.pyi +5 -3
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1/src/ducktools_classbuilder.egg-info}/PKG-INFO +1 -1
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_funcs.py +3 -3
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/MANIFEST.in +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/README.md +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/Makefile +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/approach_vs_tool.md +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/conf.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/index.md +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/make.bat +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/perf/performance_tests.md +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/prefab/index.md +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/pyproject.toml +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/setup.cfg +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/py.typed +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools_classbuilder.egg-info/SOURCES.txt +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_construction.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_internals.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_slotted_class.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/conftest.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/creation.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/creation_empty.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/dunders.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/frozen_prefabs.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/hint_syntax.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/inheritance.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/init_ex.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/kw_only.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/examples/repr_func.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_creation.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_dunders.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_frozen.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_hint_syntax.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_inheritance.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_init.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_kw_only.py +0 -0
- {ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_repr.py +0 -0
|
@@ -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
|
|
37
|
-
accessed
|
|
38
|
-
|
|
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
|
-
`
|
|
40
|
+
`get_fields(cls)` will return the resolved information obtained from this class and subclasses.
|
|
41
41
|
|
|
42
|
-
`
|
|
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
|
|
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
|
-
####
|
|
690
|
+
#### What about using annotations instead of `Field(init=False, ...)` ####
|
|
676
691
|
|
|
677
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
719
|
-
|
|
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,
|
|
727
|
-
b: Annotated[str,
|
|
728
|
-
c: Annotated[list[str],
|
|
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
|
-
|
|
732
|
-
print(
|
|
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
|
```
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/__init__.py
RENAMED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/prefab.py
RENAMED
|
@@ -31,7 +31,7 @@ import sys
|
|
|
31
31
|
from . import (
|
|
32
32
|
INTERNALS_DICT, NOTHING,
|
|
33
33
|
Field, MethodMaker, SlotFields,
|
|
34
|
-
builder, fieldclass,
|
|
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 =
|
|
90
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
|
350
|
+
fields_delimited = ", ".join(f"{field!r}" for field in attributes)
|
|
350
351
|
field_set = f"{{ {fields_delimited} }}"
|
|
351
352
|
|
|
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):
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
637
|
-
|
|
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
|
|
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.
|
|
930
|
+
if attrib.serialize and not attrib.exclude_field
|
|
920
931
|
}
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/prefab.pyi
RENAMED
|
@@ -63,7 +63,7 @@ class Attribute(Field):
|
|
|
63
63
|
compare: bool
|
|
64
64
|
iter: bool
|
|
65
65
|
kw_only: bool
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_funcs.py
RENAMED
|
@@ -45,18 +45,18 @@ def test_as_dict_excludes():
|
|
|
45
45
|
@prefab
|
|
46
46
|
class ExcludesUncached:
|
|
47
47
|
name: str
|
|
48
|
-
password: str = attribute(
|
|
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(
|
|
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(
|
|
59
|
+
password=attribute(serialize=False, type=str)
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
@prefab(dict_method=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/docs/perf/performance_tests.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/src/ducktools/classbuilder/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/dynamic/test_internals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_creation.py
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_dunders.py
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_frozen.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_init.py
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_kw_only.py
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.1.1 → ducktools_classbuilder-0.2.1}/tests/prefab/shared/test_repr.py
RENAMED
|
File without changes
|