ducktools-classbuilder 0.10.2__py3-none-any.whl → 0.11.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 +106 -65
- ducktools/classbuilder/__init__.pyi +11 -13
- ducktools/classbuilder/_version.py +2 -2
- ducktools/classbuilder/annotations/__init__.py +63 -0
- ducktools/classbuilder/annotations/annotations_314.py +104 -0
- ducktools/classbuilder/annotations/annotations_pre_314.py +42 -0
- ducktools/classbuilder/annotations.pyi +4 -4
- ducktools/classbuilder/prefab.py +108 -28
- ducktools/classbuilder/prefab.pyi +8 -1
- {ducktools_classbuilder-0.10.2.dist-info → ducktools_classbuilder-0.11.1.dist-info}/METADATA +2 -2
- ducktools_classbuilder-0.11.1.dist-info/RECORD +15 -0
- ducktools/classbuilder/annotations.py +0 -137
- ducktools_classbuilder-0.10.2.dist-info/RECORD +0 -13
- {ducktools_classbuilder-0.10.2.dist-info → ducktools_classbuilder-0.11.1.dist-info}/WHEEL +0 -0
- {ducktools_classbuilder-0.10.2.dist-info → ducktools_classbuilder-0.11.1.dist-info}/licenses/LICENSE +0 -0
- {ducktools_classbuilder-0.10.2.dist-info → ducktools_classbuilder-0.11.1.dist-info}/top_level.txt +0 -0
|
@@ -31,9 +31,20 @@
|
|
|
31
31
|
# Field itself sidesteps this by defining __slots__ to avoid that branch.
|
|
32
32
|
|
|
33
33
|
import os
|
|
34
|
-
import sys
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
try:
|
|
36
|
+
# Use the internal C module if it is available
|
|
37
|
+
from _types import ( # type: ignore
|
|
38
|
+
MemberDescriptorType as _MemberDescriptorType,
|
|
39
|
+
MappingProxyType as _MappingProxyType
|
|
40
|
+
)
|
|
41
|
+
except ImportError:
|
|
42
|
+
from types import (
|
|
43
|
+
MemberDescriptorType as _MemberDescriptorType,
|
|
44
|
+
MappingProxyType as _MappingProxyType,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
from .annotations import get_ns_annotations, is_classvar
|
|
37
48
|
from ._version import __version__, __version_tuple__ # noqa: F401
|
|
38
49
|
|
|
39
50
|
# Change this name if you make heavy modifications
|
|
@@ -45,13 +56,6 @@ GATHERED_DATA = "__classbuilder_gathered_fields__"
|
|
|
45
56
|
# overwritten. When running this is a performance penalty so it is not required.
|
|
46
57
|
_UNDER_TESTING = os.environ.get("PYTEST_VERSION") is not None
|
|
47
58
|
|
|
48
|
-
# Obtain types the same way types.py does in pypy
|
|
49
|
-
# See: https://github.com/pypy/pypy/blob/19d9fa6be11165116dd0839b9144d969ab426ae7/lib-python/3/types.py#L61-L73
|
|
50
|
-
class _C: __slots__ = 's' # noqa
|
|
51
|
-
_MemberDescriptorType = type(_C.s) # type: ignore
|
|
52
|
-
_MappingProxyType = type(type.__dict__)
|
|
53
|
-
del _C
|
|
54
|
-
|
|
55
59
|
|
|
56
60
|
def get_fields(cls, *, local=False):
|
|
57
61
|
"""
|
|
@@ -88,6 +92,20 @@ def get_methods(cls):
|
|
|
88
92
|
return getattr(cls, INTERNALS_DICT)["methods"]
|
|
89
93
|
|
|
90
94
|
|
|
95
|
+
def build_completed(ns):
|
|
96
|
+
"""
|
|
97
|
+
Utility function to determine if a class has completed the construction
|
|
98
|
+
process.
|
|
99
|
+
|
|
100
|
+
:param ns: class namespace
|
|
101
|
+
:return: True if built, False otherwise
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
return ns[INTERNALS_DICT]["build_complete"]
|
|
105
|
+
except KeyError:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
91
109
|
def _get_inst_fields(inst):
|
|
92
110
|
# This is an internal helper for constructing new
|
|
93
111
|
# 'Field' instances from existing ones.
|
|
@@ -148,8 +166,14 @@ class GeneratedCode:
|
|
|
148
166
|
|
|
149
167
|
def __eq__(self, other):
|
|
150
168
|
if self.__class__ is other.__class__:
|
|
151
|
-
return (
|
|
152
|
-
|
|
169
|
+
return (
|
|
170
|
+
self.source_code,
|
|
171
|
+
self.globs,
|
|
172
|
+
self.annotations,
|
|
173
|
+
) == (
|
|
174
|
+
other.source_code,
|
|
175
|
+
other.globs,
|
|
176
|
+
other.annotations,
|
|
153
177
|
)
|
|
154
178
|
return NotImplemented
|
|
155
179
|
|
|
@@ -208,17 +232,7 @@ class MethodMaker:
|
|
|
208
232
|
|
|
209
233
|
# Apply annotations
|
|
210
234
|
if gen.annotations is not None:
|
|
211
|
-
|
|
212
|
-
# If __annotations__ exists on the class, either they
|
|
213
|
-
# are user defined or they are using __future__ annotations.
|
|
214
|
-
# In this case, just write __annotations__
|
|
215
|
-
if "__annotations__" in gen_cls.__dict__:
|
|
216
|
-
method.__annotations__ = gen.annotations
|
|
217
|
-
else:
|
|
218
|
-
anno_func = make_annotate_func(gen.annotations)
|
|
219
|
-
method.__annotate__ = anno_func
|
|
220
|
-
else:
|
|
221
|
-
method.__annotations__ = gen.annotations
|
|
235
|
+
method.__annotations__ = gen.annotations
|
|
222
236
|
|
|
223
237
|
# Replace this descriptor on the class with the generated function
|
|
224
238
|
setattr(gen_cls, self.funcname, method)
|
|
@@ -515,6 +529,9 @@ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
|
|
|
515
529
|
"""
|
|
516
530
|
The main builder for class generation
|
|
517
531
|
|
|
532
|
+
If the GATHERED_DATA attribute exists on the class it will be used instead of
|
|
533
|
+
the provided gatherer.
|
|
534
|
+
|
|
518
535
|
:param cls: Class to be analysed and have methods generated
|
|
519
536
|
:param gatherer: Function to gather field information
|
|
520
537
|
:type gatherer: Callable[[type], tuple[dict[str, Field], dict[str, Any]]]
|
|
@@ -535,12 +552,25 @@ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
|
|
|
535
552
|
gatherer=gatherer,
|
|
536
553
|
methods=methods,
|
|
537
554
|
flags=flags,
|
|
555
|
+
fix_signature=fix_signature,
|
|
538
556
|
)
|
|
539
557
|
|
|
540
|
-
internals
|
|
558
|
+
# Get from the class dict to avoid getting an inherited internals dict
|
|
559
|
+
internals = cls.__dict__.get(INTERNALS_DICT, {})
|
|
541
560
|
setattr(cls, INTERNALS_DICT, internals)
|
|
542
561
|
|
|
543
|
-
|
|
562
|
+
# Update or add flags to internals dict
|
|
563
|
+
flag_dict = internals.get("flags", {})
|
|
564
|
+
if flags is not None:
|
|
565
|
+
flag_dict.update(flags)
|
|
566
|
+
internals["flags"] = flag_dict
|
|
567
|
+
|
|
568
|
+
cls_gathered = cls.__dict__.get(GATHERED_DATA)
|
|
569
|
+
|
|
570
|
+
if cls_gathered:
|
|
571
|
+
cls_fields, modifications = cls_gathered
|
|
572
|
+
else:
|
|
573
|
+
cls_fields, modifications = gatherer(cls)
|
|
544
574
|
|
|
545
575
|
for name, value in modifications.items():
|
|
546
576
|
if value is NOTHING:
|
|
@@ -558,11 +588,10 @@ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
|
|
|
558
588
|
for c in reversed(mro):
|
|
559
589
|
try:
|
|
560
590
|
fields.update(get_fields(c, local=True))
|
|
561
|
-
except AttributeError:
|
|
591
|
+
except (AttributeError, KeyError):
|
|
562
592
|
pass
|
|
563
593
|
|
|
564
594
|
internals["fields"] = fields
|
|
565
|
-
internals["flags"] = flags if flags is not None else {}
|
|
566
595
|
|
|
567
596
|
# Assign all of the method generators
|
|
568
597
|
internal_methods = {}
|
|
@@ -576,6 +605,9 @@ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
|
|
|
576
605
|
if fix_signature:
|
|
577
606
|
setattr(cls, "__signature__", signature_maker)
|
|
578
607
|
|
|
608
|
+
# Add attribute indicating build completed
|
|
609
|
+
internals["build_complete"] = True
|
|
610
|
+
|
|
579
611
|
return cls
|
|
580
612
|
|
|
581
613
|
|
|
@@ -604,7 +636,35 @@ class SlotMakerMeta(type):
|
|
|
604
636
|
|
|
605
637
|
Will not convert `ClassVar` hinted values.
|
|
606
638
|
"""
|
|
607
|
-
def __new__(
|
|
639
|
+
def __new__(
|
|
640
|
+
cls,
|
|
641
|
+
name,
|
|
642
|
+
bases,
|
|
643
|
+
ns,
|
|
644
|
+
slots=True,
|
|
645
|
+
gatherer=None,
|
|
646
|
+
ignore_annotations=None,
|
|
647
|
+
**kwargs
|
|
648
|
+
):
|
|
649
|
+
|
|
650
|
+
# Slot makers should inherit flags
|
|
651
|
+
for base in bases:
|
|
652
|
+
try:
|
|
653
|
+
flags = getattr(base, INTERNALS_DICT)["flags"].copy()
|
|
654
|
+
except (AttributeError, KeyError):
|
|
655
|
+
pass
|
|
656
|
+
else:
|
|
657
|
+
break
|
|
658
|
+
else:
|
|
659
|
+
flags = {"ignore_annotations": False}
|
|
660
|
+
|
|
661
|
+
# Set up flags as these may be needed early
|
|
662
|
+
if ignore_annotations is not None:
|
|
663
|
+
flags["ignore_annotations"] = ignore_annotations
|
|
664
|
+
|
|
665
|
+
# Assign flags to internals
|
|
666
|
+
ns[INTERNALS_DICT] = {"flags": flags}
|
|
667
|
+
|
|
608
668
|
# This should only run if slots=True is declared
|
|
609
669
|
# and __slots__ have not already been defined
|
|
610
670
|
if slots and "__slots__" not in ns:
|
|
@@ -632,16 +692,16 @@ class SlotMakerMeta(type):
|
|
|
632
692
|
else:
|
|
633
693
|
ns[k] = v
|
|
634
694
|
|
|
635
|
-
|
|
695
|
+
slot_values = {}
|
|
636
696
|
fields = {}
|
|
637
697
|
|
|
638
698
|
for k, v in cls_fields.items():
|
|
639
|
-
|
|
699
|
+
slot_values[k] = v.doc
|
|
640
700
|
if k not in {"__weakref__", "__dict__"}:
|
|
641
701
|
fields[k] = v
|
|
642
702
|
|
|
643
703
|
# Place slots *after* everything else to be safe
|
|
644
|
-
ns["__slots__"] =
|
|
704
|
+
ns["__slots__"] = slot_values
|
|
645
705
|
|
|
646
706
|
# Place pre-gathered field data - modifications are already applied
|
|
647
707
|
modifications = {}
|
|
@@ -811,7 +871,7 @@ def _build_field():
|
|
|
811
871
|
"init": Field(default=True, doc=field_docs["init"]),
|
|
812
872
|
"repr": Field(default=True, doc=field_docs["repr"]),
|
|
813
873
|
"compare": Field(default=True, doc=field_docs["compare"]),
|
|
814
|
-
"kw_only": Field(default=False, doc=field_docs["kw_only"])
|
|
874
|
+
"kw_only": Field(default=False, doc=field_docs["kw_only"]),
|
|
815
875
|
}
|
|
816
876
|
modifications = {"__slots__": field_docs}
|
|
817
877
|
|
|
@@ -831,21 +891,6 @@ _build_field()
|
|
|
831
891
|
del _build_field
|
|
832
892
|
|
|
833
893
|
|
|
834
|
-
def pre_gathered_gatherer(cls_or_ns):
|
|
835
|
-
"""
|
|
836
|
-
Retrieve fields previously gathered by SlotMakerMeta
|
|
837
|
-
|
|
838
|
-
:param cls_or_ns: Class to gather field information from (or class namespace)
|
|
839
|
-
:return: dict of field_name: Field(...) and modifications to be performed by the builder
|
|
840
|
-
"""
|
|
841
|
-
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
842
|
-
cls_dict = cls_or_ns
|
|
843
|
-
else:
|
|
844
|
-
cls_dict = cls_or_ns.__dict__
|
|
845
|
-
|
|
846
|
-
return cls_dict[GATHERED_DATA]
|
|
847
|
-
|
|
848
|
-
|
|
849
894
|
def make_slot_gatherer(field_type=Field):
|
|
850
895
|
"""
|
|
851
896
|
Create a new annotation gatherer that will work with `Field` instances
|
|
@@ -928,8 +973,10 @@ def make_annotation_gatherer(
|
|
|
928
973
|
"""
|
|
929
974
|
def field_annotation_gatherer(cls_or_ns):
|
|
930
975
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
976
|
+
cls = None
|
|
931
977
|
cls_dict = cls_or_ns
|
|
932
978
|
else:
|
|
979
|
+
cls = cls_or_ns
|
|
933
980
|
cls_dict = cls_or_ns.__dict__
|
|
934
981
|
|
|
935
982
|
# This should really be dict[str, field_type] but static analysis
|
|
@@ -937,7 +984,7 @@ def make_annotation_gatherer(
|
|
|
937
984
|
cls_fields: dict[str, Field] = {}
|
|
938
985
|
modifications = {}
|
|
939
986
|
|
|
940
|
-
cls_annotations = get_ns_annotations(cls_dict)
|
|
987
|
+
cls_annotations = get_ns_annotations(cls_dict, cls=cls)
|
|
941
988
|
|
|
942
989
|
kw_flag = False
|
|
943
990
|
|
|
@@ -946,7 +993,7 @@ def make_annotation_gatherer(
|
|
|
946
993
|
if is_classvar(v):
|
|
947
994
|
continue
|
|
948
995
|
|
|
949
|
-
if v is KW_ONLY or (isinstance(v, str) and
|
|
996
|
+
if v is KW_ONLY or (isinstance(v, str) and "KW_ONLY" in v):
|
|
950
997
|
if kw_flag:
|
|
951
998
|
raise SyntaxError("KW_ONLY sentinel may only appear once.")
|
|
952
999
|
kw_flag = True
|
|
@@ -984,13 +1031,14 @@ def make_annotation_gatherer(
|
|
|
984
1031
|
def make_field_gatherer(
|
|
985
1032
|
field_type=Field,
|
|
986
1033
|
leave_default_values=False,
|
|
987
|
-
assign_types=True,
|
|
988
1034
|
):
|
|
989
1035
|
def field_attribute_gatherer(cls_or_ns):
|
|
990
1036
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
991
1037
|
cls_dict = cls_or_ns
|
|
1038
|
+
cls = None
|
|
992
1039
|
else:
|
|
993
1040
|
cls_dict = cls_or_ns.__dict__
|
|
1041
|
+
cls = cls_or_ns
|
|
994
1042
|
|
|
995
1043
|
cls_attributes = {
|
|
996
1044
|
k: v
|
|
@@ -998,11 +1046,6 @@ def make_field_gatherer(
|
|
|
998
1046
|
if isinstance(v, field_type)
|
|
999
1047
|
}
|
|
1000
1048
|
|
|
1001
|
-
if assign_types:
|
|
1002
|
-
cls_annotations = get_ns_annotations(cls_dict)
|
|
1003
|
-
else:
|
|
1004
|
-
cls_annotations = {}
|
|
1005
|
-
|
|
1006
1049
|
cls_modifications = {}
|
|
1007
1050
|
|
|
1008
1051
|
for name in cls_attributes.keys():
|
|
@@ -1012,9 +1055,6 @@ def make_field_gatherer(
|
|
|
1012
1055
|
else:
|
|
1013
1056
|
cls_modifications[name] = NOTHING
|
|
1014
1057
|
|
|
1015
|
-
if assign_types and (anno := cls_annotations.get(name, NOTHING)) is not NOTHING:
|
|
1016
|
-
cls_attributes[name] = field_type.from_field(attrib, type=anno)
|
|
1017
|
-
|
|
1018
1058
|
return cls_attributes, cls_modifications
|
|
1019
1059
|
return field_attribute_gatherer
|
|
1020
1060
|
|
|
@@ -1022,7 +1062,6 @@ def make_field_gatherer(
|
|
|
1022
1062
|
def make_unified_gatherer(
|
|
1023
1063
|
field_type=Field,
|
|
1024
1064
|
leave_default_values=False,
|
|
1025
|
-
ignore_annotations=False,
|
|
1026
1065
|
):
|
|
1027
1066
|
"""
|
|
1028
1067
|
Create a gatherer that will work via first slots, then
|
|
@@ -1031,7 +1070,6 @@ def make_unified_gatherer(
|
|
|
1031
1070
|
|
|
1032
1071
|
:param field_type: The field class to use for gathering
|
|
1033
1072
|
:param leave_default_values: leave default values in place
|
|
1034
|
-
:param ignore_annotations: don't attempt to read annotations
|
|
1035
1073
|
:return: gatherer function
|
|
1036
1074
|
"""
|
|
1037
1075
|
slot_g = make_slot_gatherer(field_type)
|
|
@@ -1041,25 +1079,26 @@ def make_unified_gatherer(
|
|
|
1041
1079
|
def field_unified_gatherer(cls_or_ns):
|
|
1042
1080
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
1043
1081
|
cls_dict = cls_or_ns
|
|
1082
|
+
cls = None
|
|
1044
1083
|
else:
|
|
1045
1084
|
cls_dict = cls_or_ns.__dict__
|
|
1046
|
-
|
|
1047
|
-
cls_gathered = cls_dict.get(GATHERED_DATA)
|
|
1048
|
-
if cls_gathered:
|
|
1049
|
-
return pre_gathered_gatherer(cls_dict)
|
|
1085
|
+
cls = cls_or_ns
|
|
1050
1086
|
|
|
1051
1087
|
cls_slots = cls_dict.get("__slots__")
|
|
1052
1088
|
|
|
1053
1089
|
if isinstance(cls_slots, SlotFields):
|
|
1054
1090
|
return slot_g(cls_dict)
|
|
1055
1091
|
|
|
1092
|
+
# Get ignore_annotations flag
|
|
1093
|
+
ignore_annotations = cls_dict.get(INTERNALS_DICT, {}).get("flags", {}).get("ignore_annotations", False)
|
|
1094
|
+
|
|
1056
1095
|
if ignore_annotations:
|
|
1057
1096
|
return attrib_g(cls_dict)
|
|
1058
1097
|
else:
|
|
1059
1098
|
# To choose between annotation and attribute gatherers
|
|
1060
1099
|
# compare sets of names.
|
|
1061
1100
|
# Don't bother evaluating string annotations, as we only need names
|
|
1062
|
-
cls_annotations = get_ns_annotations(cls_dict)
|
|
1101
|
+
cls_annotations = get_ns_annotations(cls_dict, cls=cls)
|
|
1063
1102
|
cls_attributes = {
|
|
1064
1103
|
k: v for k, v in cls_dict.items() if isinstance(v, field_type)
|
|
1065
1104
|
}
|
|
@@ -1069,7 +1108,9 @@ def make_unified_gatherer(
|
|
|
1069
1108
|
|
|
1070
1109
|
if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
|
|
1071
1110
|
# All `Field` values have annotations, so use annotation gatherer
|
|
1072
|
-
|
|
1111
|
+
# Pass the original cls_or_ns object
|
|
1112
|
+
|
|
1113
|
+
return anno_g(cls_or_ns)
|
|
1073
1114
|
|
|
1074
1115
|
return attrib_g(cls_dict)
|
|
1075
1116
|
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import types
|
|
2
3
|
import typing
|
|
3
4
|
import typing_extensions
|
|
4
5
|
|
|
5
|
-
import inspect
|
|
6
6
|
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from types import MappingProxyType
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
if sys.version_info >= (3, 14):
|
|
11
|
+
import annotationlib
|
|
12
|
+
|
|
13
|
+
_py_type = annotationlib.ForwardRef | type | str
|
|
14
|
+
else:
|
|
15
|
+
_py_type = type | str
|
|
16
|
+
|
|
11
17
|
_CopiableMappings = dict[str, typing.Any] | MappingProxyType[str, typing.Any]
|
|
12
18
|
|
|
13
19
|
__version__: str
|
|
@@ -22,6 +28,8 @@ def get_flags(cls: type) -> dict[str, bool]: ...
|
|
|
22
28
|
|
|
23
29
|
def get_methods(cls: type) -> types.MappingProxyType[str, MethodMaker]: ...
|
|
24
30
|
|
|
31
|
+
def build_completed(ns: _CopiableMappings) -> bool: ...
|
|
32
|
+
|
|
25
33
|
def _get_inst_fields(inst: typing.Any) -> dict[str, typing.Any]: ...
|
|
26
34
|
|
|
27
35
|
class _NothingType:
|
|
@@ -130,6 +138,7 @@ class SlotMakerMeta(type):
|
|
|
130
138
|
ns: dict[str, typing.Any],
|
|
131
139
|
slots: bool = ...,
|
|
132
140
|
gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]] | None = ...,
|
|
141
|
+
ignore_annotations: bool | None = ...,
|
|
133
142
|
**kwargs: typing.Any,
|
|
134
143
|
) -> _T: ...
|
|
135
144
|
|
|
@@ -168,16 +177,11 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
168
177
|
@classmethod
|
|
169
178
|
def from_field(cls, fld: Field, /, **kwargs: typing.Any) -> Field: ...
|
|
170
179
|
|
|
171
|
-
|
|
172
180
|
# type[Field] doesn't work due to metaclass
|
|
173
181
|
# This is not really precise enough because isinstance is used
|
|
174
182
|
_ReturnsField = Callable[..., Field]
|
|
175
183
|
_FieldType = typing.TypeVar("_FieldType", bound=Field)
|
|
176
184
|
|
|
177
|
-
def pre_gathered_gatherer(
|
|
178
|
-
cls_or_ns: type | _CopiableMappings
|
|
179
|
-
) -> tuple[dict[str, Field | _FieldType], dict[str, typing.Any]]: ...
|
|
180
|
-
|
|
181
185
|
@typing.overload
|
|
182
186
|
def make_slot_gatherer(
|
|
183
187
|
field_type: type[_FieldType]
|
|
@@ -204,7 +208,6 @@ def make_annotation_gatherer(
|
|
|
204
208
|
def make_field_gatherer(
|
|
205
209
|
field_type: type[_FieldType],
|
|
206
210
|
leave_default_values: bool = False,
|
|
207
|
-
assign_types: bool = True,
|
|
208
211
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
|
|
209
212
|
|
|
210
213
|
@typing.overload
|
|
@@ -217,14 +220,12 @@ def make_field_gatherer(
|
|
|
217
220
|
def make_unified_gatherer(
|
|
218
221
|
field_type: type[_FieldType],
|
|
219
222
|
leave_default_values: bool = ...,
|
|
220
|
-
ignore_annotations: bool = ...,
|
|
221
223
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
|
|
222
224
|
|
|
223
225
|
@typing.overload
|
|
224
226
|
def make_unified_gatherer(
|
|
225
227
|
field_type: _ReturnsField = ...,
|
|
226
228
|
leave_default_values: bool = ...,
|
|
227
|
-
ignore_annotations: bool = ...,
|
|
228
229
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
|
|
229
230
|
|
|
230
231
|
|
|
@@ -263,9 +264,6 @@ class GatheredFields:
|
|
|
263
264
|
fields: dict[str, Field]
|
|
264
265
|
modifications: dict[str, typing.Any]
|
|
265
266
|
|
|
266
|
-
__classbuilder_internals__: dict
|
|
267
|
-
__signature__: inspect.Signature
|
|
268
|
-
|
|
269
267
|
def __init__(
|
|
270
268
|
self,
|
|
271
269
|
fields: dict[str, Field],
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "0.
|
|
2
|
-
__version_tuple__ = (0,
|
|
1
|
+
__version__ = "0.11.1"
|
|
2
|
+
__version_tuple__ = (0, 11, 1)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2024 David C Ellis
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
if sys.version_info >= (3, 14):
|
|
26
|
+
from .annotations_314 import (
|
|
27
|
+
get_func_annotations,
|
|
28
|
+
get_ns_annotations,
|
|
29
|
+
)
|
|
30
|
+
else:
|
|
31
|
+
from .annotations_pre_314 import (
|
|
32
|
+
get_func_annotations,
|
|
33
|
+
get_ns_annotations,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"get_func_annotations",
|
|
39
|
+
"get_ns_annotations",
|
|
40
|
+
"is_classvar",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_classvar(hint):
|
|
45
|
+
if isinstance(hint, str):
|
|
46
|
+
# String annotations, just check if the string 'ClassVar' is in there
|
|
47
|
+
# This is overly broad and could be smarter.
|
|
48
|
+
return "ClassVar" in hint
|
|
49
|
+
else:
|
|
50
|
+
_typing = sys.modules.get("typing")
|
|
51
|
+
if _typing:
|
|
52
|
+
_Annotated = _typing.Annotated
|
|
53
|
+
_get_origin = _typing.get_origin
|
|
54
|
+
|
|
55
|
+
if _Annotated and _get_origin(hint) is _Annotated:
|
|
56
|
+
hint = getattr(hint, "__origin__", None)
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
hint is _typing.ClassVar
|
|
60
|
+
or getattr(hint, "__origin__", None) is _typing.ClassVar
|
|
61
|
+
):
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2024 David C Ellis
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
Python 3.14 has new annotations methods, but does not provide any correct way to handle VALUE
|
|
25
|
+
annotation generation for new __init__ methods.
|
|
26
|
+
|
|
27
|
+
The approach taken here is to try to use VALUE annotations, if those fail it falls back to
|
|
28
|
+
STRING annotations, as if __future__ annotations actually arrived.
|
|
29
|
+
|
|
30
|
+
Hopefully in a future version of Python we will have complete, correct, performant annotations
|
|
31
|
+
so we can use a more standard format.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
class _LazyAnnotationLib:
|
|
35
|
+
def __getattr__(self, item):
|
|
36
|
+
global _lazy_annotationlib
|
|
37
|
+
import annotationlib # type: ignore
|
|
38
|
+
_lazy_annotationlib = annotationlib
|
|
39
|
+
return getattr(annotationlib, item)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_lazy_annotationlib = _LazyAnnotationLib()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_func_annotations(func, use_forwardref=False):
|
|
46
|
+
"""
|
|
47
|
+
Given a function, return the annotations dictionary
|
|
48
|
+
|
|
49
|
+
:param func: function object
|
|
50
|
+
:return: dictionary of annotations
|
|
51
|
+
"""
|
|
52
|
+
# Try to get `__annotations__` for VALUE annotations first
|
|
53
|
+
try:
|
|
54
|
+
raw_annotations = func.__annotations__
|
|
55
|
+
except Exception:
|
|
56
|
+
fmt = (
|
|
57
|
+
_lazy_annotationlib.Format.FORWARDREF
|
|
58
|
+
if use_forwardref
|
|
59
|
+
else _lazy_annotationlib.Format.STRING
|
|
60
|
+
)
|
|
61
|
+
annotations = _lazy_annotationlib.get_annotations(func, format=fmt)
|
|
62
|
+
else:
|
|
63
|
+
annotations = raw_annotations.copy()
|
|
64
|
+
|
|
65
|
+
return annotations
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_ns_annotations(ns, cls=None, use_forwardref=False):
|
|
69
|
+
"""
|
|
70
|
+
Given a class namespace, attempt to retrieve the
|
|
71
|
+
annotations dictionary.
|
|
72
|
+
|
|
73
|
+
:param ns: Class namespace (eg cls.__dict__)
|
|
74
|
+
:param cls: Class if available
|
|
75
|
+
:param use_forwardref: Use FORWARDREF instead of STRING if VALUE fails
|
|
76
|
+
:return: dictionary of annotations
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
annotations = ns.get("__annotations__")
|
|
80
|
+
if annotations is not None:
|
|
81
|
+
annotations = annotations.copy()
|
|
82
|
+
else:
|
|
83
|
+
# See if we're using PEP-649 annotations
|
|
84
|
+
annotate = _lazy_annotationlib.get_annotate_from_class_namespace(ns)
|
|
85
|
+
if annotate:
|
|
86
|
+
try:
|
|
87
|
+
annotations = annotate(_lazy_annotationlib.Format.VALUE)
|
|
88
|
+
except Exception:
|
|
89
|
+
fmt = (
|
|
90
|
+
_lazy_annotationlib.Format.FORWARDREF
|
|
91
|
+
if use_forwardref
|
|
92
|
+
else _lazy_annotationlib.Format.STRING
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
annotations = _lazy_annotationlib.call_annotate_function(
|
|
96
|
+
annotate,
|
|
97
|
+
format=fmt,
|
|
98
|
+
owner=cls
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if annotations is None:
|
|
102
|
+
annotations = {}
|
|
103
|
+
|
|
104
|
+
return annotations
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2024 David C Ellis
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
def get_func_annotations(func, use_forwardref=False):
|
|
24
|
+
"""
|
|
25
|
+
Given a function, return the annotations dictionary
|
|
26
|
+
|
|
27
|
+
:param func: function object
|
|
28
|
+
:return: dictionary of annotations
|
|
29
|
+
"""
|
|
30
|
+
annotations = func.__annotations__
|
|
31
|
+
return annotations
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# This is simplified under 3.13 or earlier
|
|
35
|
+
def get_ns_annotations(ns, cls=None, use_forwardref=False):
|
|
36
|
+
annotations = ns.get("__annotations__")
|
|
37
|
+
if annotations is not None:
|
|
38
|
+
annotations = annotations.copy()
|
|
39
|
+
else:
|
|
40
|
+
annotations = {}
|
|
41
|
+
return annotations
|
|
42
|
+
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
import typing
|
|
3
3
|
import types
|
|
4
|
+
import sys
|
|
4
5
|
|
|
5
6
|
_CopiableMappings = dict[str, typing.Any] | types.MappingProxyType[str, typing.Any]
|
|
6
7
|
|
|
7
8
|
def get_func_annotations(
|
|
8
9
|
func: types.FunctionType,
|
|
10
|
+
use_forwardref: bool = ...,
|
|
9
11
|
) -> dict[str, typing.Any]: ...
|
|
10
12
|
|
|
11
13
|
def get_ns_annotations(
|
|
12
14
|
ns: _CopiableMappings,
|
|
15
|
+
cls: type | None = ...,
|
|
16
|
+
use_forwardref: bool = ...,
|
|
13
17
|
) -> dict[str, typing.Any]: ...
|
|
14
18
|
|
|
15
|
-
def make_annotate_func(
|
|
16
|
-
annos: dict[str, typing.Any]
|
|
17
|
-
) -> Callable[[int], dict[str, typing.Any]]: ...
|
|
18
|
-
|
|
19
19
|
def is_classvar(
|
|
20
20
|
hint: object,
|
|
21
21
|
) -> bool: ...
|
ducktools/classbuilder/prefab.py
CHANGED
|
@@ -32,13 +32,13 @@ from . import (
|
|
|
32
32
|
make_unified_gatherer,
|
|
33
33
|
eq_maker, frozen_setattr_maker, frozen_delattr_maker, replace_maker,
|
|
34
34
|
get_repr_generator,
|
|
35
|
+
build_completed,
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
from .annotations import get_func_annotations
|
|
38
39
|
|
|
39
40
|
# These aren't used but are re-exported for ease of use
|
|
40
|
-
|
|
41
|
-
from . import SlotFields, KW_ONLY # noqa: F401
|
|
41
|
+
from . import SlotFields as SlotFields, KW_ONLY as KW_ONLY
|
|
42
42
|
|
|
43
43
|
PREFAB_FIELDS = "PREFAB_FIELDS"
|
|
44
44
|
PREFAB_INIT_FUNC = "__prefab_init__"
|
|
@@ -327,7 +327,7 @@ def attribute(
|
|
|
327
327
|
:param private: Short for init, repr, compare, iter, serialize = False, must have default or factory
|
|
328
328
|
:param doc: Parameter documentation for slotted classes
|
|
329
329
|
:param metadata: Dictionary for additional non-construction metadata
|
|
330
|
-
:param type: Type of this attribute
|
|
330
|
+
:param type: Type of this attribute
|
|
331
331
|
|
|
332
332
|
:return: Attribute generated with these parameters.
|
|
333
333
|
"""
|
|
@@ -361,7 +361,10 @@ def attribute(
|
|
|
361
361
|
)
|
|
362
362
|
|
|
363
363
|
|
|
364
|
-
prefab_gatherer = make_unified_gatherer(
|
|
364
|
+
prefab_gatherer = make_unified_gatherer(
|
|
365
|
+
Attribute,
|
|
366
|
+
leave_default_values=False,
|
|
367
|
+
)
|
|
365
368
|
|
|
366
369
|
|
|
367
370
|
# Class Builders
|
|
@@ -376,9 +379,11 @@ def _make_prefab(
|
|
|
376
379
|
match_args=True,
|
|
377
380
|
kw_only=False,
|
|
378
381
|
frozen=False,
|
|
382
|
+
replace=True,
|
|
379
383
|
dict_method=False,
|
|
380
384
|
recursive_repr=False,
|
|
381
385
|
gathered_fields=None,
|
|
386
|
+
ignore_annotations=False,
|
|
382
387
|
):
|
|
383
388
|
"""
|
|
384
389
|
Generate boilerplate code for dunder methods in a class.
|
|
@@ -393,21 +398,22 @@ def _make_prefab(
|
|
|
393
398
|
:param frozen: Prevent attribute values from being changed once defined
|
|
394
399
|
(This does not prevent the modification of mutable attributes
|
|
395
400
|
such as lists)
|
|
401
|
+
:param replace: Add a generated __replace__ method
|
|
396
402
|
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
397
403
|
:param recursive_repr: Safely handle repr in case of recursion
|
|
398
404
|
:param gathered_fields: Pre-gathered fields callable, to skip re-collecting attributes
|
|
405
|
+
:param ignore_annotations: Ignore annotated fields and only look at `attribute` fields
|
|
399
406
|
:return: class with __ methods defined
|
|
400
407
|
"""
|
|
401
408
|
cls_dict = cls.__dict__
|
|
402
409
|
|
|
403
|
-
if
|
|
410
|
+
if build_completed(cls_dict):
|
|
404
411
|
raise PrefabError(
|
|
405
412
|
f"Decorated class {cls.__name__!r} "
|
|
406
413
|
f"has already been processed as a Prefab."
|
|
407
414
|
)
|
|
408
415
|
|
|
409
416
|
slots = cls_dict.get("__slots__")
|
|
410
|
-
|
|
411
417
|
slotted = False if slots is None else True
|
|
412
418
|
|
|
413
419
|
if gathered_fields is None:
|
|
@@ -438,11 +444,22 @@ def _make_prefab(
|
|
|
438
444
|
if dict_method:
|
|
439
445
|
methods.add(asdict_maker)
|
|
440
446
|
|
|
441
|
-
|
|
447
|
+
if replace and "__replace__" not in cls_dict:
|
|
448
|
+
methods.add(replace_maker)
|
|
442
449
|
|
|
443
450
|
flags = {
|
|
444
|
-
"kw_only": kw_only,
|
|
445
451
|
"slotted": slotted,
|
|
452
|
+
"init": init,
|
|
453
|
+
"repr": repr,
|
|
454
|
+
"eq": eq,
|
|
455
|
+
"iter": iter,
|
|
456
|
+
"match_args": match_args,
|
|
457
|
+
"kw_only": kw_only,
|
|
458
|
+
"frozen": frozen,
|
|
459
|
+
"replace": replace,
|
|
460
|
+
"dict_method": dict_method,
|
|
461
|
+
"recursive_repr": recursive_repr,
|
|
462
|
+
"ignore_annotations": ignore_annotations,
|
|
446
463
|
}
|
|
447
464
|
|
|
448
465
|
cls = builder(
|
|
@@ -551,30 +568,69 @@ def _make_prefab(
|
|
|
551
568
|
class Prefab(metaclass=SlotMakerMeta, gatherer=prefab_gatherer):
|
|
552
569
|
__slots__ = {} # type: ignore
|
|
553
570
|
|
|
554
|
-
# noinspection PyShadowingBuiltins
|
|
555
571
|
def __init_subclass__(
|
|
556
572
|
cls,
|
|
557
|
-
|
|
558
|
-
repr=True,
|
|
559
|
-
eq=True,
|
|
560
|
-
iter=False,
|
|
561
|
-
match_args=True,
|
|
562
|
-
kw_only=False,
|
|
563
|
-
frozen=False,
|
|
564
|
-
dict_method=False,
|
|
565
|
-
recursive_repr=False,
|
|
573
|
+
**kwargs
|
|
566
574
|
):
|
|
575
|
+
"""
|
|
576
|
+
Generate boilerplate code for dunder methods in a class.
|
|
577
|
+
|
|
578
|
+
Use as a base class, slotted by default
|
|
579
|
+
|
|
580
|
+
:param init: generates __init__ if true or __prefab_init__ if false
|
|
581
|
+
:param repr: generate __repr__
|
|
582
|
+
:param eq: generate __eq__
|
|
583
|
+
:param iter: generate __iter__
|
|
584
|
+
:param match_args: generate __match_args__
|
|
585
|
+
:param kw_only: make all attributes keyword only
|
|
586
|
+
:param frozen: Prevent attribute values from being changed once defined
|
|
587
|
+
(This does not prevent the modification of mutable attributes such as lists)
|
|
588
|
+
:param replace: generate a __replace__ method
|
|
589
|
+
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
590
|
+
:param recursive_repr: Safely handle repr in case of recursion
|
|
591
|
+
:param ignore_annotations: Ignore type annotations when gathering fields, only look for
|
|
592
|
+
slots or attribute(...) values
|
|
593
|
+
:param slots: automatically generate slots for this class's attributes
|
|
594
|
+
"""
|
|
595
|
+
default_values = {
|
|
596
|
+
"init": True,
|
|
597
|
+
"repr": True,
|
|
598
|
+
"eq": True,
|
|
599
|
+
"iter": False,
|
|
600
|
+
"match_args": True,
|
|
601
|
+
"kw_only": False,
|
|
602
|
+
"frozen": False,
|
|
603
|
+
"replace": True,
|
|
604
|
+
"dict_method": False,
|
|
605
|
+
"recursive_repr": False,
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
flags = get_flags(cls).copy()
|
|
610
|
+
except (AttributeError, KeyError):
|
|
611
|
+
flags = {}
|
|
612
|
+
else:
|
|
613
|
+
# Remove the value of slotted if it exists
|
|
614
|
+
flags.pop("slotted", None)
|
|
615
|
+
|
|
616
|
+
for k in default_values:
|
|
617
|
+
kwarg_value = kwargs.pop(k, None)
|
|
618
|
+
default = default_values[k]
|
|
619
|
+
|
|
620
|
+
if kwarg_value is not None:
|
|
621
|
+
flags[k] = kwarg_value
|
|
622
|
+
elif flags.get(k) is None:
|
|
623
|
+
flags[k] = default
|
|
624
|
+
|
|
625
|
+
if kwargs:
|
|
626
|
+
error_args = ", ".join(repr(k) for k in kwargs)
|
|
627
|
+
raise TypeError(
|
|
628
|
+
f"Prefab.__init_subclass__ got unexpected keyword arguments {error_args}"
|
|
629
|
+
)
|
|
630
|
+
|
|
567
631
|
_make_prefab(
|
|
568
632
|
cls,
|
|
569
|
-
|
|
570
|
-
repr=repr,
|
|
571
|
-
eq=eq,
|
|
572
|
-
iter=iter,
|
|
573
|
-
match_args=match_args,
|
|
574
|
-
kw_only=kw_only,
|
|
575
|
-
frozen=frozen,
|
|
576
|
-
dict_method=dict_method,
|
|
577
|
-
recursive_repr=recursive_repr,
|
|
633
|
+
**flags
|
|
578
634
|
)
|
|
579
635
|
|
|
580
636
|
|
|
@@ -589,8 +645,10 @@ def prefab(
|
|
|
589
645
|
match_args=True,
|
|
590
646
|
kw_only=False,
|
|
591
647
|
frozen=False,
|
|
648
|
+
replace=True,
|
|
592
649
|
dict_method=False,
|
|
593
650
|
recursive_repr=False,
|
|
651
|
+
ignore_annotations=False,
|
|
594
652
|
):
|
|
595
653
|
"""
|
|
596
654
|
Generate boilerplate code for dunder methods in a class.
|
|
@@ -606,8 +664,11 @@ def prefab(
|
|
|
606
664
|
:param kw_only: make all attributes keyword only
|
|
607
665
|
:param frozen: Prevent attribute values from being changed once defined
|
|
608
666
|
(This does not prevent the modification of mutable attributes such as lists)
|
|
667
|
+
:param replace: generate a __replace__ method
|
|
609
668
|
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
610
669
|
:param recursive_repr: Safely handle repr in case of recursion
|
|
670
|
+
:param ignore_annotations: Ignore type annotations when gathering fields, only look for
|
|
671
|
+
slots or attribute(...) values
|
|
611
672
|
|
|
612
673
|
:return: class with __ methods defined
|
|
613
674
|
"""
|
|
@@ -622,8 +683,10 @@ def prefab(
|
|
|
622
683
|
match_args=match_args,
|
|
623
684
|
kw_only=kw_only,
|
|
624
685
|
frozen=frozen,
|
|
686
|
+
replace=replace,
|
|
625
687
|
dict_method=dict_method,
|
|
626
688
|
recursive_repr=recursive_repr,
|
|
689
|
+
ignore_annotations=ignore_annotations,
|
|
627
690
|
)
|
|
628
691
|
else:
|
|
629
692
|
return _make_prefab(
|
|
@@ -635,8 +698,10 @@ def prefab(
|
|
|
635
698
|
match_args=match_args,
|
|
636
699
|
kw_only=kw_only,
|
|
637
700
|
frozen=frozen,
|
|
701
|
+
replace=replace,
|
|
638
702
|
dict_method=dict_method,
|
|
639
703
|
recursive_repr=recursive_repr,
|
|
704
|
+
ignore_annotations=ignore_annotations,
|
|
640
705
|
)
|
|
641
706
|
|
|
642
707
|
|
|
@@ -654,6 +719,7 @@ def build_prefab(
|
|
|
654
719
|
match_args=True,
|
|
655
720
|
kw_only=False,
|
|
656
721
|
frozen=False,
|
|
722
|
+
replace=True,
|
|
657
723
|
dict_method=False,
|
|
658
724
|
recursive_repr=False,
|
|
659
725
|
slots=False,
|
|
@@ -675,6 +741,7 @@ def build_prefab(
|
|
|
675
741
|
:param kw_only: make all attributes keyword only
|
|
676
742
|
:param frozen: Prevent attribute values from being changed once defined
|
|
677
743
|
(This does not prevent the modification of mutable attributes such as lists)
|
|
744
|
+
:param replace: generate a __replace__ method
|
|
678
745
|
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
679
746
|
:param recursive_repr: Safely handle repr in case of recursion
|
|
680
747
|
:param slots: Make the resulting class slotted
|
|
@@ -703,6 +770,7 @@ def build_prefab(
|
|
|
703
770
|
class_dict["__slots__"] = class_slots
|
|
704
771
|
|
|
705
772
|
class_dict["__annotations__"] = class_annotations
|
|
773
|
+
|
|
706
774
|
cls = type(class_name, bases, class_dict)
|
|
707
775
|
|
|
708
776
|
gathered_fields = GatheredFields(fields, {})
|
|
@@ -716,6 +784,7 @@ def build_prefab(
|
|
|
716
784
|
match_args=match_args,
|
|
717
785
|
kw_only=kw_only,
|
|
718
786
|
frozen=frozen,
|
|
787
|
+
replace=replace,
|
|
719
788
|
dict_method=dict_method,
|
|
720
789
|
recursive_repr=recursive_repr,
|
|
721
790
|
gathered_fields=gathered_fields,
|
|
@@ -779,6 +848,17 @@ def as_dict(o):
|
|
|
779
848
|
}
|
|
780
849
|
|
|
781
850
|
def replace(obj, /, **changes):
|
|
851
|
+
"""
|
|
852
|
+
Create a copy of a prefab instance with values provided to 'changes' replaced
|
|
853
|
+
|
|
854
|
+
:param obj: prefab instance
|
|
855
|
+
:return: new prefab instance
|
|
856
|
+
"""
|
|
782
857
|
if not is_prefab_instance(obj):
|
|
783
858
|
raise TypeError("replace() should be called on prefab instances")
|
|
784
|
-
|
|
859
|
+
try:
|
|
860
|
+
replace_func = obj.__replace__
|
|
861
|
+
except AttributeError:
|
|
862
|
+
raise TypeError(f"{obj.__class__.__name__!r} does not support __replace__")
|
|
863
|
+
|
|
864
|
+
return replace_func(**changes)
|
|
@@ -2,7 +2,6 @@ import typing
|
|
|
2
2
|
from types import MappingProxyType
|
|
3
3
|
from typing_extensions import dataclass_transform
|
|
4
4
|
|
|
5
|
-
import inspect
|
|
6
5
|
|
|
7
6
|
# Suppress weird pylance error
|
|
8
7
|
from collections.abc import Callable # type: ignore
|
|
@@ -141,9 +140,11 @@ def _make_prefab(
|
|
|
141
140
|
match_args: bool = True,
|
|
142
141
|
kw_only: bool = False,
|
|
143
142
|
frozen: bool = False,
|
|
143
|
+
replace: bool = True,
|
|
144
144
|
dict_method: bool = False,
|
|
145
145
|
recursive_repr: bool = False,
|
|
146
146
|
gathered_fields: Callable[[type], tuple[dict[str, Attribute], dict[str, typing.Any]]] | None = None,
|
|
147
|
+
ignore_annotations: bool = False,
|
|
147
148
|
) -> type: ...
|
|
148
149
|
|
|
149
150
|
_T = typing.TypeVar("_T")
|
|
@@ -151,10 +152,12 @@ _T = typing.TypeVar("_T")
|
|
|
151
152
|
# noinspection PyUnresolvedReferences
|
|
152
153
|
@dataclass_transform(field_specifiers=(Attribute, attribute))
|
|
153
154
|
class Prefab(metaclass=SlotMakerMeta):
|
|
155
|
+
__classbuilder_internals__: dict[str, typing.Any]
|
|
154
156
|
_meta_gatherer: Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]] = ...
|
|
155
157
|
__slots__: dict[str, typing.Any] = ...
|
|
156
158
|
def __init_subclass__(
|
|
157
159
|
cls,
|
|
160
|
+
*,
|
|
158
161
|
init: bool = True,
|
|
159
162
|
repr: bool = True,
|
|
160
163
|
eq: bool = True,
|
|
@@ -162,6 +165,7 @@ class Prefab(metaclass=SlotMakerMeta):
|
|
|
162
165
|
match_args: bool = True,
|
|
163
166
|
kw_only: bool = False,
|
|
164
167
|
frozen: bool = False,
|
|
168
|
+
replace: bool = True,
|
|
165
169
|
dict_method: bool = False,
|
|
166
170
|
recursive_repr: bool = False,
|
|
167
171
|
) -> None: ...
|
|
@@ -213,8 +217,10 @@ def prefab(
|
|
|
213
217
|
match_args: bool = ...,
|
|
214
218
|
kw_only: bool = ...,
|
|
215
219
|
frozen: bool = ...,
|
|
220
|
+
replace: bool = ...,
|
|
216
221
|
dict_method: bool = ...,
|
|
217
222
|
recursive_repr: bool = ...,
|
|
223
|
+
ignore_annotations: bool = ...,
|
|
218
224
|
) -> typing.Any: ...
|
|
219
225
|
|
|
220
226
|
def build_prefab(
|
|
@@ -230,6 +236,7 @@ def build_prefab(
|
|
|
230
236
|
match_args: bool = True,
|
|
231
237
|
kw_only: bool = False,
|
|
232
238
|
frozen: bool = False,
|
|
239
|
+
replace: bool = True,
|
|
233
240
|
dict_method: bool = False,
|
|
234
241
|
recursive_repr: bool = False,
|
|
235
242
|
slots: bool = False,
|
{ducktools_classbuilder-0.10.2.dist-info → ducktools_classbuilder-0.11.1.dist-info}/METADATA
RENAMED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ducktools-classbuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: Toolkit for creating class boilerplate generators
|
|
5
5
|
Author: David C Ellis
|
|
6
|
+
License-Expression: MIT
|
|
6
7
|
Project-URL: Homepage, https://github.com/davidcellis/ducktools-classbuilder
|
|
7
8
|
Classifier: Development Status :: 4 - Beta
|
|
8
9
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -11,7 +12,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
11
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ducktools/classbuilder/__init__.py,sha256=MnzhfqFtCvrjXaEgl23bpqNdcnYo2gDcJ2NJ4tZoAb4,38774
|
|
2
|
+
ducktools/classbuilder/__init__.pyi,sha256=K-M3seIjYBdp5LV5q-rsPmFnd7XriUAh6yt6kDGJHRQ,8177
|
|
3
|
+
ducktools/classbuilder/_version.py,sha256=FOGeLRFwLtviAiW-KIp4wHLv9i4slcrAsV1dT5vOTeA,54
|
|
4
|
+
ducktools/classbuilder/annotations.pyi,sha256=bKTwQlPydbwrbVGaDu_PoSYOhuaqv8I_tMHf-g4aT0M,476
|
|
5
|
+
ducktools/classbuilder/prefab.py,sha256=b32StMHZtP0nzhWs5cwNmftUmmQT3yg5fsCDbo56s0w,27369
|
|
6
|
+
ducktools/classbuilder/prefab.pyi,sha256=_cg9WsZqwkgOy0iowHtzGB2E6BSNJdKcdk0IC_Wz_qU,6756
|
|
7
|
+
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
8
|
+
ducktools/classbuilder/annotations/__init__.py,sha256=n8VFmOItEbsMuq5gothp5yi6H3m3JJdVys04DVb0r-4,2145
|
|
9
|
+
ducktools/classbuilder/annotations/annotations_314.py,sha256=Yeng_kke1dcE6RarOzYN7SczWc9TSQEEZFEODzSj4uo,3692
|
|
10
|
+
ducktools/classbuilder/annotations/annotations_pre_314.py,sha256=Et-TYQYNVtMssShsS62PTu2mou2kr-ZJldYAAayhjAU,1651
|
|
11
|
+
ducktools_classbuilder-0.11.1.dist-info/licenses/LICENSE,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
12
|
+
ducktools_classbuilder-0.11.1.dist-info/METADATA,sha256=qyayp3GPPE2dnjdbU3ojhty4VS29tnbJxE5PARqHfh0,9204
|
|
13
|
+
ducktools_classbuilder-0.11.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
ducktools_classbuilder-0.11.1.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
15
|
+
ducktools_classbuilder-0.11.1.dist-info/RECORD,,
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# MIT License
|
|
2
|
-
#
|
|
3
|
-
# Copyright (c) 2024 David C Ellis
|
|
4
|
-
#
|
|
5
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
# in the Software without restriction, including without limitation the rights
|
|
8
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
# furnished to do so, subject to the following conditions:
|
|
11
|
-
#
|
|
12
|
-
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
# copies or substantial portions of the Software.
|
|
14
|
-
#
|
|
15
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
# SOFTWARE.
|
|
22
|
-
import sys
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class _LazyAnnotationLib:
|
|
26
|
-
def __getattr__(self, item):
|
|
27
|
-
global _lazy_annotationlib
|
|
28
|
-
import annotationlib # type: ignore
|
|
29
|
-
_lazy_annotationlib = annotationlib
|
|
30
|
-
return getattr(annotationlib, item)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
_lazy_annotationlib = _LazyAnnotationLib()
|
|
34
|
-
|
|
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
|
-
|
|
59
|
-
def get_ns_annotations(ns):
|
|
60
|
-
"""
|
|
61
|
-
Given a class namespace, attempt to retrieve the
|
|
62
|
-
annotations dictionary.
|
|
63
|
-
|
|
64
|
-
:param ns: Class namespace (eg cls.__dict__)
|
|
65
|
-
:return: dictionary of annotations
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
annotations = ns.get("__annotations__")
|
|
69
|
-
if annotations is not None:
|
|
70
|
-
annotations = annotations.copy()
|
|
71
|
-
elif sys.version_info >= (3, 14):
|
|
72
|
-
# See if we're using PEP-649 annotations
|
|
73
|
-
annotate = _lazy_annotationlib.get_annotate_from_class_namespace(ns)
|
|
74
|
-
if annotate:
|
|
75
|
-
annotations = _lazy_annotationlib.call_annotate_function(
|
|
76
|
-
annotate,
|
|
77
|
-
format=_lazy_annotationlib.Format.FORWARDREF
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
if annotations is None:
|
|
81
|
-
annotations = {}
|
|
82
|
-
|
|
83
|
-
return annotations
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def make_annotate_func(annos):
|
|
87
|
-
# Only used in 3.14 or later so no sys.version_info gate
|
|
88
|
-
|
|
89
|
-
type_repr = _lazy_annotationlib.type_repr
|
|
90
|
-
Format = _lazy_annotationlib.Format
|
|
91
|
-
ForwardRef = _lazy_annotationlib.ForwardRef
|
|
92
|
-
# Construct an annotation function from __annotations__
|
|
93
|
-
def annotate_func(format, /):
|
|
94
|
-
match format:
|
|
95
|
-
case Format.VALUE | Format.FORWARDREF:
|
|
96
|
-
return {
|
|
97
|
-
k: v.evaluate(format=format)
|
|
98
|
-
if isinstance(v, ForwardRef) else v
|
|
99
|
-
for k, v in annos.items()
|
|
100
|
-
}
|
|
101
|
-
case Format.STRING:
|
|
102
|
-
string_annos = {}
|
|
103
|
-
for k, v in annos.items():
|
|
104
|
-
if isinstance(v, str):
|
|
105
|
-
string_annos[k] = v
|
|
106
|
-
elif isinstance(v, ForwardRef):
|
|
107
|
-
string_annos[k] = v.evaluate(format=Format.STRING)
|
|
108
|
-
else:
|
|
109
|
-
string_annos[k] = type_repr(v)
|
|
110
|
-
return string_annos
|
|
111
|
-
case _:
|
|
112
|
-
raise NotImplementedError(format)
|
|
113
|
-
return annotate_func
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def is_classvar(hint):
|
|
117
|
-
if isinstance(hint, str):
|
|
118
|
-
# String annotations, just check if the string 'ClassVar' is in there
|
|
119
|
-
# This is overly broad and could be smarter.
|
|
120
|
-
return "ClassVar" in hint
|
|
121
|
-
elif (annotationlib := sys.modules.get("annotationlib")) and isinstance(hint, annotationlib.ForwardRef):
|
|
122
|
-
return "ClassVar" in hint.__arg__
|
|
123
|
-
else:
|
|
124
|
-
_typing = sys.modules.get("typing")
|
|
125
|
-
if _typing:
|
|
126
|
-
_Annotated = _typing.Annotated
|
|
127
|
-
_get_origin = _typing.get_origin
|
|
128
|
-
|
|
129
|
-
if _Annotated and _get_origin(hint) is _Annotated:
|
|
130
|
-
hint = getattr(hint, "__origin__", None)
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
hint is _typing.ClassVar
|
|
134
|
-
or getattr(hint, "__origin__", None) is _typing.ClassVar
|
|
135
|
-
):
|
|
136
|
-
return True
|
|
137
|
-
return False
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
ducktools/classbuilder/__init__.py,sha256=uTJI45-T5ZENOfN5CqnPJRujendDYwQsu0A-zsOdIho,38198
|
|
2
|
-
ducktools/classbuilder/__init__.pyi,sha256=gL_YMTFovtD0vTPeBH-wkiy_l761bxItruIBRs6Aouw,8302
|
|
3
|
-
ducktools/classbuilder/_version.py,sha256=DH400d3tmKMEKy7OEfbyeJl0NAXPlOLDwet_95nLc-M,54
|
|
4
|
-
ducktools/classbuilder/annotations.py,sha256=JtL8RhNp5hB_dfUju_zsqI19F9yTRQ1uM5ZTpas-VVw,4753
|
|
5
|
-
ducktools/classbuilder/annotations.pyi,sha256=oUTN_ee7DWCz4CI36I7mAD43Bxlz_e5ises_VNnCX9g,480
|
|
6
|
-
ducktools/classbuilder/prefab.py,sha256=UiYBvVSWM4BB4JGwUkYtG9QhPK8TzCYUiey_dgNejo8,24345
|
|
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.2.dist-info/licenses/LICENSE,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
10
|
-
ducktools_classbuilder-0.10.2.dist-info/METADATA,sha256=bL2jfRX1oqIJGKySrWwdEHsX_8gpOHCFPRy9i66fQnQ,9231
|
|
11
|
-
ducktools_classbuilder-0.10.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
ducktools_classbuilder-0.10.2.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
13
|
-
ducktools_classbuilder-0.10.2.dist-info/RECORD,,
|
|
File without changes
|
{ducktools_classbuilder-0.10.2.dist-info → ducktools_classbuilder-0.11.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.10.2.dist-info → ducktools_classbuilder-0.11.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|