ducktools-classbuilder 0.9.1__py3-none-any.whl → 0.10.1__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.
- ducktools/classbuilder/__init__.py +178 -77
- ducktools/classbuilder/__init__.pyi +19 -11
- ducktools/classbuilder/_version.py +2 -2
- ducktools/classbuilder/annotations.py +25 -13
- ducktools/classbuilder/annotations.pyi +4 -0
- ducktools/classbuilder/prefab.py +6 -5
- ducktools/classbuilder/prefab.pyi +3 -1
- {ducktools_classbuilder-0.9.1.dist-info → ducktools_classbuilder-0.10.1.dist-info}/METADATA +7 -17
- ducktools_classbuilder-0.10.1.dist-info/RECORD +13 -0
- {ducktools_classbuilder-0.9.1.dist-info → ducktools_classbuilder-0.10.1.dist-info}/WHEEL +1 -1
- ducktools_classbuilder-0.9.1.dist-info/RECORD +0 -13
- {ducktools_classbuilder-0.9.1.dist-info → ducktools_classbuilder-0.10.1.dist-info/licenses}/LICENSE +0 -0
- {ducktools_classbuilder-0.9.1.dist-info → ducktools_classbuilder-0.10.1.dist-info}/top_level.txt +0 -0
|
@@ -38,7 +38,7 @@ from ._version import __version__, __version_tuple__ # noqa: F401
|
|
|
38
38
|
# Change this name if you make heavy modifications
|
|
39
39
|
INTERNALS_DICT = "__classbuilder_internals__"
|
|
40
40
|
META_GATHERER_NAME = "_meta_gatherer"
|
|
41
|
-
|
|
41
|
+
GATHERED_DATA = "__classbuilder_gathered_fields__"
|
|
42
42
|
|
|
43
43
|
# If testing, make Field classes frozen to make sure attributes are not
|
|
44
44
|
# overwritten. When running this is a performance penalty so it is not required.
|
|
@@ -114,12 +114,16 @@ FIELD_NOTHING = _NothingType("FIELD")
|
|
|
114
114
|
# KW_ONLY sentinel 'type' to use to indicate all subsequent attributes are
|
|
115
115
|
# keyword only
|
|
116
116
|
# noinspection PyPep8Naming
|
|
117
|
-
class
|
|
117
|
+
class _KW_ONLY_META(type):
|
|
118
118
|
def __repr__(self):
|
|
119
|
-
return "<KW_ONLY Sentinel
|
|
119
|
+
return "<KW_ONLY Sentinel>"
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
KW_ONLY
|
|
122
|
+
class KW_ONLY(metaclass=_KW_ONLY_META):
|
|
123
|
+
"""
|
|
124
|
+
Sentinel Class to indicate that variables declared after
|
|
125
|
+
this sentinel are to be converted to KW_ONLY arguments.
|
|
126
|
+
"""
|
|
123
127
|
|
|
124
128
|
|
|
125
129
|
class GeneratedCode:
|
|
@@ -213,15 +217,23 @@ class _SignatureMaker:
|
|
|
213
217
|
# help(cls) will fail along with inspect.signature(cls)
|
|
214
218
|
# This signature maker descriptor is placed to override __signature__ and force
|
|
215
219
|
# the `__init__` signature to be generated first if the signature is requested.
|
|
216
|
-
def __get__(self, instance, cls):
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
def __get__(self, instance, cls=None):
|
|
221
|
+
if cls is None:
|
|
222
|
+
cls = type(instance)
|
|
223
|
+
|
|
224
|
+
# force generation of `__init__` function
|
|
225
|
+
_ = cls.__init__
|
|
226
|
+
|
|
227
|
+
if instance is None:
|
|
228
|
+
raise AttributeError(
|
|
229
|
+
f"type object {cls.__name__!r} "
|
|
230
|
+
"has no attribute '__signature__'"
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
raise AttributeError(
|
|
234
|
+
f"{cls.__name__!r} object"
|
|
235
|
+
"has no attribute '__signature__'"
|
|
236
|
+
)
|
|
225
237
|
|
|
226
238
|
|
|
227
239
|
signature_maker = _SignatureMaker()
|
|
@@ -389,7 +401,7 @@ def replace_generator(cls, funcname="__replace__"):
|
|
|
389
401
|
# Generate the replace method for built classes
|
|
390
402
|
# unlike the dataclasses implementation this is generated
|
|
391
403
|
attribs = get_fields(cls)
|
|
392
|
-
|
|
404
|
+
|
|
393
405
|
# This is essentially the as_dict generator for prefabs
|
|
394
406
|
# except based on attrib.init instead of .serialize
|
|
395
407
|
vals = ", ".join(
|
|
@@ -403,7 +415,7 @@ def replace_generator(cls, funcname="__replace__"):
|
|
|
403
415
|
f"def {funcname}(self, /, **changes):\n"
|
|
404
416
|
f" new_kwargs = {init_dict}\n"
|
|
405
417
|
f" for name, value in changes.items():\n"
|
|
406
|
-
f" if name not in new_kwargs:\n"
|
|
418
|
+
f" if name not in new_kwargs:\n"
|
|
407
419
|
f" raise TypeError(\n"
|
|
408
420
|
f" f\"{{name!r}} is not a valid replacable \"\n"
|
|
409
421
|
f" f\"field on {{self.__class__.__name__!r}}\"\n"
|
|
@@ -571,21 +583,25 @@ class SlotMakerMeta(type):
|
|
|
571
583
|
|
|
572
584
|
Will not convert `ClassVar` hinted values.
|
|
573
585
|
"""
|
|
574
|
-
def __new__(cls, name, bases, ns, slots=True, **kwargs):
|
|
586
|
+
def __new__(cls, name, bases, ns, slots=True, gatherer=None, **kwargs):
|
|
575
587
|
# This should only run if slots=True is declared
|
|
576
588
|
# and __slots__ have not already been defined
|
|
577
589
|
if slots and "__slots__" not in ns:
|
|
578
590
|
# Check if a different gatherer has been set in any base classes
|
|
579
591
|
# Default to unified gatherer
|
|
580
|
-
gatherer
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
592
|
+
if gatherer is None:
|
|
593
|
+
gatherer = ns.get(META_GATHERER_NAME, None)
|
|
594
|
+
if not gatherer:
|
|
595
|
+
for base in bases:
|
|
596
|
+
if g := getattr(base, META_GATHERER_NAME, None):
|
|
597
|
+
gatherer = g
|
|
598
|
+
break
|
|
599
|
+
|
|
600
|
+
if not gatherer:
|
|
601
|
+
gatherer = unified_gatherer
|
|
586
602
|
|
|
587
|
-
|
|
588
|
-
|
|
603
|
+
# Set the gatherer in the namespace
|
|
604
|
+
ns[META_GATHERER_NAME] = gatherer
|
|
589
605
|
|
|
590
606
|
# Obtain slots from annotations or attributes
|
|
591
607
|
cls_fields, cls_modifications = gatherer(ns)
|
|
@@ -595,14 +611,54 @@ class SlotMakerMeta(type):
|
|
|
595
611
|
else:
|
|
596
612
|
ns[k] = v
|
|
597
613
|
|
|
614
|
+
slots = {}
|
|
615
|
+
fields = {}
|
|
616
|
+
|
|
617
|
+
for k, v in cls_fields.items():
|
|
618
|
+
slots[k] = v.doc
|
|
619
|
+
if k not in {"__weakref__", "__dict__"}:
|
|
620
|
+
fields[k] = v
|
|
621
|
+
|
|
598
622
|
# Place slots *after* everything else to be safe
|
|
599
|
-
ns["__slots__"] =
|
|
623
|
+
ns["__slots__"] = slots
|
|
624
|
+
|
|
625
|
+
# Place pre-gathered field data - modifications are already applied
|
|
626
|
+
modifications = {}
|
|
627
|
+
ns[GATHERED_DATA] = fields, modifications
|
|
628
|
+
|
|
629
|
+
else:
|
|
630
|
+
if gatherer is not None:
|
|
631
|
+
ns[META_GATHERER_NAME] = gatherer
|
|
600
632
|
|
|
601
633
|
new_cls = super().__new__(cls, name, bases, ns, **kwargs)
|
|
602
634
|
|
|
603
635
|
return new_cls
|
|
604
636
|
|
|
605
637
|
|
|
638
|
+
# This class is set up before fields as it will be used to generate the Fields
|
|
639
|
+
# for Field itself so Field can have generated __eq__, __repr__ and other methods
|
|
640
|
+
class GatheredFields:
|
|
641
|
+
"""
|
|
642
|
+
Helper class to store gathered field data
|
|
643
|
+
"""
|
|
644
|
+
__slots__ = ("fields", "modifications")
|
|
645
|
+
|
|
646
|
+
def __init__(self, fields, modifications):
|
|
647
|
+
self.fields = fields
|
|
648
|
+
self.modifications = modifications
|
|
649
|
+
|
|
650
|
+
def __eq__(self, other):
|
|
651
|
+
if type(self) is type(other):
|
|
652
|
+
return self.fields == other.fields and self.modifications == other.modifications
|
|
653
|
+
|
|
654
|
+
def __repr__(self):
|
|
655
|
+
return f"{type(self).__name__}(fields={self.fields!r}, modifications={self.modifications!r})"
|
|
656
|
+
|
|
657
|
+
def __call__(self, cls_dict):
|
|
658
|
+
# cls_dict will be provided, but isn't needed
|
|
659
|
+
return self.fields, self.modifications
|
|
660
|
+
|
|
661
|
+
|
|
606
662
|
# The Field class can finally be defined.
|
|
607
663
|
# The __init__ method has to be written manually so Fields can be created
|
|
608
664
|
# However after this, the other methods can be generated.
|
|
@@ -628,17 +684,18 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
628
684
|
:param compare: Include in the class __eq__.
|
|
629
685
|
:param kw_only: Make this a keyword only parameter in __init__.
|
|
630
686
|
"""
|
|
631
|
-
|
|
632
|
-
#
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
687
|
+
|
|
688
|
+
# Plain slots are required as part of bootstrapping
|
|
689
|
+
# This prevents SlotMakerMeta from trying to generate 'Field's
|
|
690
|
+
__slots__ = (
|
|
691
|
+
"default",
|
|
692
|
+
"default_factory",
|
|
693
|
+
"type",
|
|
694
|
+
"doc",
|
|
695
|
+
"init",
|
|
696
|
+
"repr",
|
|
697
|
+
"compare",
|
|
698
|
+
"kw_only",
|
|
642
699
|
)
|
|
643
700
|
|
|
644
701
|
# noinspection PyShadowingBuiltins
|
|
@@ -672,6 +729,7 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
672
729
|
self.validate_field()
|
|
673
730
|
|
|
674
731
|
def __init_subclass__(cls, frozen=False):
|
|
732
|
+
# Subclasses of Field can be created as if they are dataclasses
|
|
675
733
|
field_methods = {_field_init_maker, repr_maker, eq_maker}
|
|
676
734
|
if frozen or _UNDER_TESTING:
|
|
677
735
|
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
@@ -707,6 +765,66 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
707
765
|
return cls(**argument_dict)
|
|
708
766
|
|
|
709
767
|
|
|
768
|
+
def _build_field():
|
|
769
|
+
# Complete the construction of the Field class
|
|
770
|
+
field_docs = {
|
|
771
|
+
"default": "Standard default value to be used for attributes with this field.",
|
|
772
|
+
"default_factory":
|
|
773
|
+
"A zero-argument function to be called to generate a default value, "
|
|
774
|
+
"useful for mutable obects like lists.",
|
|
775
|
+
"type": "The type of the attribute to be assigned by this field.",
|
|
776
|
+
"doc":
|
|
777
|
+
"The documentation for the attribute that appears when calling "
|
|
778
|
+
"help(...) on the class. (Only in slotted classes).",
|
|
779
|
+
"init": "Include this attribute in the class __init__ parameters.",
|
|
780
|
+
"repr": "Include this attribute in the class __repr__",
|
|
781
|
+
"compare": "Include this attribute in the class __eq__ method",
|
|
782
|
+
"kw_only": "Make this a keyword only parameter in __init__",
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
fields = {
|
|
786
|
+
"default": Field(default=NOTHING, doc=field_docs["default"]),
|
|
787
|
+
"default_factory": Field(default=NOTHING, doc=field_docs["default_factory"]),
|
|
788
|
+
"type": Field(default=NOTHING, doc=field_docs["type"]),
|
|
789
|
+
"doc": Field(default=None, doc=field_docs["doc"]),
|
|
790
|
+
"init": Field(default=True, doc=field_docs["init"]),
|
|
791
|
+
"repr": Field(default=True, doc=field_docs["repr"]),
|
|
792
|
+
"compare": Field(default=True, doc=field_docs["compare"]),
|
|
793
|
+
"kw_only": Field(default=False, doc=field_docs["kw_only"])
|
|
794
|
+
}
|
|
795
|
+
modifications = {"__slots__": field_docs}
|
|
796
|
+
|
|
797
|
+
field_methods = {repr_maker, eq_maker}
|
|
798
|
+
if _UNDER_TESTING:
|
|
799
|
+
field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
800
|
+
|
|
801
|
+
builder(
|
|
802
|
+
Field,
|
|
803
|
+
gatherer=GatheredFields(fields, modifications),
|
|
804
|
+
methods=field_methods,
|
|
805
|
+
flags={"slotted": True, "kw_only": True},
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
_build_field()
|
|
810
|
+
del _build_field
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def pre_gathered_gatherer(cls_or_ns):
|
|
814
|
+
"""
|
|
815
|
+
Retrieve fields previously gathered by SlotMakerMeta
|
|
816
|
+
|
|
817
|
+
:param cls_or_ns: Class to gather field information from (or class namespace)
|
|
818
|
+
:return: dict of field_name: Field(...) and modifications to be performed by the builder
|
|
819
|
+
"""
|
|
820
|
+
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
821
|
+
cls_dict = cls_or_ns
|
|
822
|
+
else:
|
|
823
|
+
cls_dict = cls_or_ns.__dict__
|
|
824
|
+
|
|
825
|
+
return cls_dict[GATHERED_DATA]
|
|
826
|
+
|
|
827
|
+
|
|
710
828
|
def make_slot_gatherer(field_type=Field):
|
|
711
829
|
"""
|
|
712
830
|
Create a new annotation gatherer that will work with `Field` instances
|
|
@@ -721,7 +839,7 @@ def make_slot_gatherer(field_type=Field):
|
|
|
721
839
|
Gather field information for class generation based on __slots__
|
|
722
840
|
|
|
723
841
|
:param cls_or_ns: Class to gather field information from (or class namespace)
|
|
724
|
-
:return: dict of field_name: Field(...)
|
|
842
|
+
:return: dict of field_name: Field(...) and modifications to be performed by the builder
|
|
725
843
|
"""
|
|
726
844
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
727
845
|
cls_dict = cls_or_ns
|
|
@@ -883,6 +1001,7 @@ def make_field_gatherer(
|
|
|
883
1001
|
def make_unified_gatherer(
|
|
884
1002
|
field_type=Field,
|
|
885
1003
|
leave_default_values=False,
|
|
1004
|
+
ignore_annotations=False,
|
|
886
1005
|
):
|
|
887
1006
|
"""
|
|
888
1007
|
Create a gatherer that will work via first slots, then
|
|
@@ -891,6 +1010,7 @@ def make_unified_gatherer(
|
|
|
891
1010
|
|
|
892
1011
|
:param field_type: The field class to use for gathering
|
|
893
1012
|
:param leave_default_values: leave default values in place
|
|
1013
|
+
:param ignore_annotations: don't attempt to read annotations
|
|
894
1014
|
:return: gatherer function
|
|
895
1015
|
"""
|
|
896
1016
|
slot_g = make_slot_gatherer(field_type)
|
|
@@ -903,27 +1023,35 @@ def make_unified_gatherer(
|
|
|
903
1023
|
else:
|
|
904
1024
|
cls_dict = cls_or_ns.__dict__
|
|
905
1025
|
|
|
1026
|
+
cls_gathered = cls_dict.get(GATHERED_DATA)
|
|
1027
|
+
if cls_gathered:
|
|
1028
|
+
return pre_gathered_gatherer(cls_dict)
|
|
1029
|
+
|
|
906
1030
|
cls_slots = cls_dict.get("__slots__")
|
|
907
1031
|
|
|
908
1032
|
if isinstance(cls_slots, SlotFields):
|
|
909
1033
|
return slot_g(cls_dict)
|
|
910
1034
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1035
|
+
if ignore_annotations:
|
|
1036
|
+
return attrib_g(cls_dict)
|
|
1037
|
+
else:
|
|
1038
|
+
# To choose between annotation and attribute gatherers
|
|
1039
|
+
# compare sets of names.
|
|
1040
|
+
# Don't bother evaluating string annotations, as we only need names
|
|
1041
|
+
cls_annotations = get_ns_annotations(cls_dict)
|
|
1042
|
+
cls_attributes = {
|
|
1043
|
+
k: v for k, v in cls_dict.items() if isinstance(v, field_type)
|
|
1044
|
+
}
|
|
918
1045
|
|
|
919
|
-
|
|
920
|
-
|
|
1046
|
+
cls_annotation_names = cls_annotations.keys()
|
|
1047
|
+
cls_attribute_names = cls_attributes.keys()
|
|
921
1048
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1049
|
+
if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
|
|
1050
|
+
# All `Field` values have annotations, so use annotation gatherer
|
|
1051
|
+
return anno_g(cls_dict)
|
|
1052
|
+
|
|
1053
|
+
return attrib_g(cls_dict)
|
|
925
1054
|
|
|
926
|
-
return attrib_g(cls_dict)
|
|
927
1055
|
return field_unified_gatherer
|
|
928
1056
|
|
|
929
1057
|
|
|
@@ -935,19 +1063,6 @@ annotation_gatherer = make_annotation_gatherer()
|
|
|
935
1063
|
unified_gatherer = make_unified_gatherer()
|
|
936
1064
|
|
|
937
1065
|
|
|
938
|
-
# Now the gatherers have been defined, add __repr__ and __eq__ to Field.
|
|
939
|
-
_field_methods = {repr_maker, eq_maker}
|
|
940
|
-
if _UNDER_TESTING:
|
|
941
|
-
_field_methods.update({frozen_setattr_maker, frozen_delattr_maker})
|
|
942
|
-
|
|
943
|
-
builder(
|
|
944
|
-
Field,
|
|
945
|
-
gatherer=slot_gatherer,
|
|
946
|
-
methods=_field_methods,
|
|
947
|
-
flags={"slotted": True, "kw_only": True},
|
|
948
|
-
)
|
|
949
|
-
|
|
950
|
-
|
|
951
1066
|
def check_argument_order(cls):
|
|
952
1067
|
"""
|
|
953
1068
|
Raise a SyntaxError if the argument order will be invalid for a generated
|
|
@@ -990,17 +1105,3 @@ def slotclass(cls=None, /, *, methods=default_methods, syntax_check=True):
|
|
|
990
1105
|
check_argument_order(cls)
|
|
991
1106
|
|
|
992
1107
|
return cls
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
@slotclass
|
|
996
|
-
class GatheredFields:
|
|
997
|
-
"""
|
|
998
|
-
A helper gatherer for fields that have been gathered externally.
|
|
999
|
-
"""
|
|
1000
|
-
__slots__ = SlotFields(
|
|
1001
|
-
fields=Field(),
|
|
1002
|
-
modifications=Field(),
|
|
1003
|
-
)
|
|
1004
|
-
|
|
1005
|
-
def __call__(self, cls):
|
|
1006
|
-
return self.fields, self.modifications
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import types
|
|
2
2
|
import typing
|
|
3
|
+
import typing_extensions
|
|
3
4
|
|
|
4
5
|
import inspect
|
|
5
6
|
|
|
@@ -13,6 +14,7 @@ __version__: str
|
|
|
13
14
|
__version_tuple__: tuple[str | int, ...]
|
|
14
15
|
INTERNALS_DICT: str
|
|
15
16
|
META_GATHERER_NAME: str
|
|
17
|
+
GATHERED_DATA: str
|
|
16
18
|
|
|
17
19
|
def get_fields(cls: type, *, local: bool = False) -> dict[str, Field]: ...
|
|
18
20
|
|
|
@@ -28,11 +30,11 @@ class _NothingType:
|
|
|
28
30
|
NOTHING: _NothingType
|
|
29
31
|
FIELD_NOTHING: _NothingType
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
class _KW_ONLY_TYPE:
|
|
33
|
+
class _KW_ONLY_META(type):
|
|
33
34
|
def __repr__(self) -> str: ...
|
|
34
35
|
|
|
35
|
-
KW_ONLY:
|
|
36
|
+
class KW_ONLY(metaclass=_KW_ONLY_META): ...
|
|
37
|
+
|
|
36
38
|
# Stub Only
|
|
37
39
|
@typing.type_check_only
|
|
38
40
|
class _CodegenType(typing.Protocol):
|
|
@@ -56,7 +58,7 @@ class MethodMaker:
|
|
|
56
58
|
def __get__(self, instance, cls) -> Callable: ...
|
|
57
59
|
|
|
58
60
|
class _SignatureMaker:
|
|
59
|
-
def __get__(self, instance, cls) ->
|
|
61
|
+
def __get__(self, instance, cls=None) -> typing_extensions.Never: ...
|
|
60
62
|
|
|
61
63
|
signature_maker: _SignatureMaker
|
|
62
64
|
|
|
@@ -122,7 +124,8 @@ class SlotMakerMeta(type):
|
|
|
122
124
|
name: str,
|
|
123
125
|
bases: tuple[type, ...],
|
|
124
126
|
ns: dict[str, typing.Any],
|
|
125
|
-
slots: bool =
|
|
127
|
+
slots: bool = ...,
|
|
128
|
+
gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]] | None = ...,
|
|
126
129
|
**kwargs: typing.Any,
|
|
127
130
|
) -> _T: ...
|
|
128
131
|
|
|
@@ -139,7 +142,7 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
139
142
|
|
|
140
143
|
__slots__: dict[str, str]
|
|
141
144
|
__classbuilder_internals__: dict
|
|
142
|
-
__signature__:
|
|
145
|
+
__signature__: _SignatureMaker
|
|
143
146
|
|
|
144
147
|
def __init__(
|
|
145
148
|
self,
|
|
@@ -167,6 +170,9 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
167
170
|
_ReturnsField = Callable[..., Field]
|
|
168
171
|
_FieldType = typing.TypeVar("_FieldType", bound=Field)
|
|
169
172
|
|
|
173
|
+
def pre_gathered_gatherer(
|
|
174
|
+
cls_or_ns: type | _CopiableMappings
|
|
175
|
+
) -> tuple[dict[str, Field | _FieldType], dict[str, typing.Any]]: ...
|
|
170
176
|
|
|
171
177
|
@typing.overload
|
|
172
178
|
def make_slot_gatherer(
|
|
@@ -206,13 +212,15 @@ def make_field_gatherer(
|
|
|
206
212
|
@typing.overload
|
|
207
213
|
def make_unified_gatherer(
|
|
208
214
|
field_type: type[_FieldType],
|
|
209
|
-
leave_default_values: bool =
|
|
215
|
+
leave_default_values: bool = ...,
|
|
216
|
+
ignore_annotations: bool = ...,
|
|
210
217
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
|
|
211
218
|
|
|
212
219
|
@typing.overload
|
|
213
220
|
def make_unified_gatherer(
|
|
214
|
-
field_type: _ReturnsField =
|
|
215
|
-
leave_default_values: bool =
|
|
221
|
+
field_type: _ReturnsField = ...,
|
|
222
|
+
leave_default_values: bool = ...,
|
|
223
|
+
ignore_annotations: bool = ...,
|
|
216
224
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
|
|
217
225
|
|
|
218
226
|
|
|
@@ -246,7 +254,7 @@ def slotclass(
|
|
|
246
254
|
_gatherer_type = Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]
|
|
247
255
|
|
|
248
256
|
class GatheredFields:
|
|
249
|
-
__slots__:
|
|
257
|
+
__slots__: tuple[str, ...]
|
|
250
258
|
|
|
251
259
|
fields: dict[str, Field]
|
|
252
260
|
modifications: dict[str, typing.Any]
|
|
@@ -262,4 +270,4 @@ class GatheredFields:
|
|
|
262
270
|
|
|
263
271
|
def __repr__(self) -> str: ...
|
|
264
272
|
def __eq__(self, other) -> bool: ...
|
|
265
|
-
def __call__(self,
|
|
273
|
+
def __call__(self, cls_dict: type | dict[str, typing.Any]) -> tuple[dict[str, Field], dict[str, typing.Any]]: ...
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "0.
|
|
2
|
-
__version_tuple__ = (0,
|
|
1
|
+
__version__ = "0.10.1"
|
|
2
|
+
__version_tuple__ = (0, 10, 1)
|
|
@@ -33,6 +33,29 @@ class _LazyAnnotationLib:
|
|
|
33
33
|
_lazy_annotationlib = _LazyAnnotationLib()
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def get_func_annotations(func):
|
|
37
|
+
"""
|
|
38
|
+
Given a function, return the annotations dictionary
|
|
39
|
+
|
|
40
|
+
:param func: function object
|
|
41
|
+
:return: dictionary of annotations
|
|
42
|
+
"""
|
|
43
|
+
# This method exists for use by prefab in getting annotations from
|
|
44
|
+
# the __prefab_post_init__ function
|
|
45
|
+
try:
|
|
46
|
+
annotations = func.__annotations__
|
|
47
|
+
except Exception:
|
|
48
|
+
if sys.version_info >= (3, 14):
|
|
49
|
+
annotations = _lazy_annotationlib.get_annotations(
|
|
50
|
+
func,
|
|
51
|
+
format=_lazy_annotationlib.Format.FORWARDREF,
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
return annotations
|
|
57
|
+
|
|
58
|
+
|
|
36
59
|
def get_ns_annotations(ns):
|
|
37
60
|
"""
|
|
38
61
|
Given a class namespace, attempt to retrieve the
|
|
@@ -70,19 +93,8 @@ def is_classvar(hint):
|
|
|
70
93
|
else:
|
|
71
94
|
_typing = sys.modules.get("typing")
|
|
72
95
|
if _typing:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# 3.8 also needs get_origin from typing_extensions
|
|
76
|
-
if sys.version_info < (3, 10):
|
|
77
|
-
_typing_extensions = sys.modules.get("typing_extensions")
|
|
78
|
-
if _typing_extensions:
|
|
79
|
-
_Annotated = _typing_extensions.Annotated
|
|
80
|
-
_get_origin = _typing_extensions.get_origin
|
|
81
|
-
else:
|
|
82
|
-
_Annotated, _get_origin = None, None
|
|
83
|
-
else:
|
|
84
|
-
_Annotated = _typing.Annotated
|
|
85
|
-
_get_origin = _typing.get_origin
|
|
96
|
+
_Annotated = _typing.Annotated
|
|
97
|
+
_get_origin = _typing.get_origin
|
|
86
98
|
|
|
87
99
|
if _Annotated and _get_origin(hint) is _Annotated:
|
|
88
100
|
hint = getattr(hint, "__origin__", None)
|
|
@@ -3,6 +3,10 @@ import types
|
|
|
3
3
|
|
|
4
4
|
_CopiableMappings = dict[str, typing.Any] | types.MappingProxyType[str, typing.Any]
|
|
5
5
|
|
|
6
|
+
def get_func_annotations(
|
|
7
|
+
func: types.FunctionType,
|
|
8
|
+
) -> dict[str, typing.Any]: ...
|
|
9
|
+
|
|
6
10
|
def get_ns_annotations(
|
|
7
11
|
ns: _CopiableMappings,
|
|
8
12
|
) -> dict[str, typing.Any]: ...
|
ducktools/classbuilder/prefab.py
CHANGED
|
@@ -34,6 +34,8 @@ from . import (
|
|
|
34
34
|
get_repr_generator,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
+
from .annotations import get_func_annotations
|
|
38
|
+
|
|
37
39
|
# These aren't used but are re-exported for ease of use
|
|
38
40
|
# noinspection PyUnresolvedReferences
|
|
39
41
|
from . import SlotFields, KW_ONLY # noqa: F401
|
|
@@ -98,7 +100,7 @@ def init_generator(cls, funcname="__init__"):
|
|
|
98
100
|
func_arglist.extend(arglist)
|
|
99
101
|
|
|
100
102
|
if extra_funcname == POST_INIT_FUNC:
|
|
101
|
-
post_init_annotations.update(func
|
|
103
|
+
post_init_annotations.update(get_func_annotations(func))
|
|
102
104
|
|
|
103
105
|
pos_arglist = []
|
|
104
106
|
kw_only_arglist = []
|
|
@@ -204,11 +206,11 @@ def init_generator(cls, funcname="__init__"):
|
|
|
204
206
|
post_init_call = ""
|
|
205
207
|
|
|
206
208
|
code = (
|
|
207
|
-
f"def {funcname}(self, {args}):\n"
|
|
209
|
+
f"def {funcname}(self, {args}) -> None:\n"
|
|
208
210
|
f"{pre_init_call}\n"
|
|
209
211
|
f"{body}\n"
|
|
210
212
|
f"{post_init_call}\n"
|
|
211
|
-
|
|
213
|
+
)
|
|
212
214
|
|
|
213
215
|
return GeneratedCode(code, globs)
|
|
214
216
|
|
|
@@ -551,8 +553,7 @@ def _make_prefab(
|
|
|
551
553
|
return cls
|
|
552
554
|
|
|
553
555
|
|
|
554
|
-
class Prefab(metaclass=SlotMakerMeta):
|
|
555
|
-
_meta_gatherer = prefab_gatherer
|
|
556
|
+
class Prefab(metaclass=SlotMakerMeta, gatherer=prefab_gatherer):
|
|
556
557
|
__slots__ = {} # type: ignore
|
|
557
558
|
|
|
558
559
|
# noinspection PyShadowingBuiltins
|
|
@@ -13,6 +13,7 @@ from . import (
|
|
|
13
13
|
GeneratedCode,
|
|
14
14
|
MethodMaker,
|
|
15
15
|
SlotMakerMeta,
|
|
16
|
+
_SignatureMaker
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
from . import SlotFields as SlotFields, KW_ONLY as KW_ONLY
|
|
@@ -47,7 +48,8 @@ hash_maker: MethodMaker
|
|
|
47
48
|
|
|
48
49
|
class Attribute(Field):
|
|
49
50
|
__slots__: dict
|
|
50
|
-
__signature__:
|
|
51
|
+
__signature__: _SignatureMaker
|
|
52
|
+
__classbuilder_gathered_fields__: tuple[dict[str, Field], dict[str, typing.Any]]
|
|
51
53
|
|
|
52
54
|
iter: bool
|
|
53
55
|
serialize: bool
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ducktools-classbuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
4
4
|
Summary: Toolkit for creating class boilerplate generators
|
|
5
5
|
Author: David C Ellis
|
|
6
6
|
Project-URL: Homepage, https://github.com/davidcellis/ducktools-classbuilder
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
10
8
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -14,22 +12,14 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
13
|
Classifier: Operating System :: OS Independent
|
|
16
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
-
Requires-Python: >=3.
|
|
15
|
+
Requires-Python: >=3.10
|
|
18
16
|
Description-Content-Type: text/markdown
|
|
19
17
|
License-File: LICENSE
|
|
20
18
|
Provides-Extra: docs
|
|
21
|
-
Requires-Dist: sphinx
|
|
22
|
-
Requires-Dist: myst-parser
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
|
|
25
|
-
Requires-Dist: attrs ; extra == 'performance_tests'
|
|
26
|
-
Requires-Dist: pydantic ; extra == 'performance_tests'
|
|
27
|
-
Provides-Extra: testing
|
|
28
|
-
Requires-Dist: pytest >=8.2 ; extra == 'testing'
|
|
29
|
-
Requires-Dist: pytest-cov ; extra == 'testing'
|
|
30
|
-
Requires-Dist: typing-extensions ; extra == 'testing'
|
|
31
|
-
Provides-Extra: type_checking
|
|
32
|
-
Requires-Dist: mypy ; extra == 'type_checking'
|
|
19
|
+
Requires-Dist: sphinx>=8.1; extra == "docs"
|
|
20
|
+
Requires-Dist: myst-parser>=4.0; extra == "docs"
|
|
21
|
+
Requires-Dist: sphinx_rtd_theme>=3.0; extra == "docs"
|
|
22
|
+
Dynamic: license-file
|
|
33
23
|
|
|
34
24
|
# Ducktools: Class Builder #
|
|
35
25
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ducktools/classbuilder/__init__.py,sha256=zJuGJ8_1EPEYJqO0XXDuknwGD1MxPJSZXS5_wFAW6B0,37299
|
|
2
|
+
ducktools/classbuilder/__init__.pyi,sha256=NazFRfLcMpU533jGixaWHV_kARtc9J4-xXX-gkD8rFY,8177
|
|
3
|
+
ducktools/classbuilder/_version.py,sha256=kdUPXlUQWcTv2wnqFkwrCx7Ue7WU2JVPnG1UU0gmLck,54
|
|
4
|
+
ducktools/classbuilder/annotations.py,sha256=ImEEuzEFUrGNRMlAl7YN-H8PZT1FOK8D_yL1LygNdmo,3626
|
|
5
|
+
ducktools/classbuilder/annotations.pyi,sha256=zAKJbEN1klvZu71ShuvKbf6OE7ddKb5USC6MbOjRPfY,336
|
|
6
|
+
ducktools/classbuilder/prefab.py,sha256=RJfOv9yzJsySaBz6w4e5ZP8Q7K3LWX6_Buuh2zD9YDQ,24689
|
|
7
|
+
ducktools/classbuilder/prefab.pyi,sha256=eZLjFhM-oHeRBONpCWSb1wLJKGs5Xiq0w_Jeor8Go-w,6525
|
|
8
|
+
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
9
|
+
ducktools_classbuilder-0.10.1.dist-info/licenses/LICENSE,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
10
|
+
ducktools_classbuilder-0.10.1.dist-info/METADATA,sha256=LkjNjXyb49v3vYBEvbldkThl9xAgCHhVc8dPNgiMsxw,9231
|
|
11
|
+
ducktools_classbuilder-0.10.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
ducktools_classbuilder-0.10.1.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
13
|
+
ducktools_classbuilder-0.10.1.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
ducktools/classbuilder/__init__.py,sha256=5vagbDiErapPudNG5-gP3yarlA6-q8IriMcRrAzBBr4,33433
|
|
2
|
-
ducktools/classbuilder/__init__.pyi,sha256=6nEfVyeF1HE4YlvcNkFetACqMd1EObu3hYwoLYBbRNo,7796
|
|
3
|
-
ducktools/classbuilder/_version.py,sha256=Ti7IekFg9SsQHXFYa9yzblMN29C0yL2OG6aLOfnSQOc,52
|
|
4
|
-
ducktools/classbuilder/annotations.py,sha256=VEZsCM8lwfhaWrQi8dUOAkicYHxUHaSAyM-FzL34wXI,3583
|
|
5
|
-
ducktools/classbuilder/annotations.pyi,sha256=c5vYtULdDgMYWtkzeYMsHIbmnEuT2Ru-nNZieWvYuQ4,247
|
|
6
|
-
ducktools/classbuilder/prefab.py,sha256=RiVbESwFiPv3Y4PpclSZAwdwBomILbhGEF8XHAaDzW4,24643
|
|
7
|
-
ducktools/classbuilder/prefab.pyi,sha256=_Tm2V97Udt3-WlbBRuOSt11PPYdw1TrFeSd4Mcs2EKM,6422
|
|
8
|
-
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
9
|
-
ducktools_classbuilder-0.9.1.dist-info/LICENSE,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
10
|
-
ducktools_classbuilder-0.9.1.dist-info/METADATA,sha256=45E7CeSoBEii1Tra1U16WUvcBepEQ4vCsay_y_2UinY,9687
|
|
11
|
-
ducktools_classbuilder-0.9.1.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
12
|
-
ducktools_classbuilder-0.9.1.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
13
|
-
ducktools_classbuilder-0.9.1.dist-info/RECORD,,
|
{ducktools_classbuilder-0.9.1.dist-info → ducktools_classbuilder-0.10.1.dist-info/licenses}/LICENSE
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.9.1.dist-info → ducktools_classbuilder-0.10.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|