ducktools-classbuilder 0.10.1__py3-none-any.whl → 0.11.0__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 +116 -54
- ducktools/classbuilder/__init__.pyi +19 -17
- ducktools/classbuilder/_version.py +2 -2
- ducktools/classbuilder/annotations/__init__.py +63 -0
- ducktools/classbuilder/{annotations.py → annotations/annotations_314.py} +41 -44
- ducktools/classbuilder/annotations/annotations_pre_314.py +42 -0
- ducktools/classbuilder/annotations.pyi +5 -0
- ducktools/classbuilder/prefab.py +128 -51
- ducktools/classbuilder/prefab.pyi +8 -1
- {ducktools_classbuilder-0.10.1.dist-info → ducktools_classbuilder-0.11.0.dist-info}/METADATA +2 -2
- ducktools_classbuilder-0.11.0.dist-info/RECORD +15 -0
- ducktools_classbuilder-0.10.1.dist-info/RECORD +0 -13
- {ducktools_classbuilder-0.10.1.dist-info → ducktools_classbuilder-0.11.0.dist-info}/WHEEL +0 -0
- {ducktools_classbuilder-0.10.1.dist-info → ducktools_classbuilder-0.11.0.dist-info}/licenses/LICENSE +0 -0
- {ducktools_classbuilder-0.10.1.dist-info → ducktools_classbuilder-0.11.0.dist-info}/top_level.txt +0 -0
|
@@ -32,6 +32,18 @@
|
|
|
32
32
|
|
|
33
33
|
import os
|
|
34
34
|
|
|
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
|
+
|
|
35
47
|
from .annotations import get_ns_annotations, is_classvar
|
|
36
48
|
from ._version import __version__, __version_tuple__ # noqa: F401
|
|
37
49
|
|
|
@@ -44,13 +56,6 @@ GATHERED_DATA = "__classbuilder_gathered_fields__"
|
|
|
44
56
|
# overwritten. When running this is a performance penalty so it is not required.
|
|
45
57
|
_UNDER_TESTING = os.environ.get("PYTEST_VERSION") is not None
|
|
46
58
|
|
|
47
|
-
# Obtain types the same way types.py does in pypy
|
|
48
|
-
# See: https://github.com/pypy/pypy/blob/19d9fa6be11165116dd0839b9144d969ab426ae7/lib-python/3/types.py#L61-L73
|
|
49
|
-
class _C: __slots__ = 's' # noqa
|
|
50
|
-
_MemberDescriptorType = type(_C.s) # type: ignore
|
|
51
|
-
_MappingProxyType = type(type.__dict__)
|
|
52
|
-
del _C
|
|
53
|
-
|
|
54
59
|
|
|
55
60
|
def get_fields(cls, *, local=False):
|
|
56
61
|
"""
|
|
@@ -87,6 +92,20 @@ def get_methods(cls):
|
|
|
87
92
|
return getattr(cls, INTERNALS_DICT)["methods"]
|
|
88
93
|
|
|
89
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
|
+
|
|
90
109
|
def _get_inst_fields(inst):
|
|
91
110
|
# This is an internal helper for constructing new
|
|
92
111
|
# 'Field' instances from existing ones.
|
|
@@ -131,19 +150,31 @@ class GeneratedCode:
|
|
|
131
150
|
This class provides a return value for the generated output from source code
|
|
132
151
|
generators.
|
|
133
152
|
"""
|
|
134
|
-
__slots__ = ("source_code", "globs")
|
|
153
|
+
__slots__ = ("source_code", "globs", "annotations")
|
|
135
154
|
|
|
136
|
-
def __init__(self, source_code, globs):
|
|
155
|
+
def __init__(self, source_code, globs, annotations=None):
|
|
137
156
|
self.source_code = source_code
|
|
138
157
|
self.globs = globs
|
|
158
|
+
self.annotations = annotations
|
|
139
159
|
|
|
140
160
|
def __repr__(self):
|
|
141
161
|
first_source_line = self.source_code.split("\n")[0]
|
|
142
|
-
return
|
|
162
|
+
return (
|
|
163
|
+
f"GeneratorOutput(source_code='{first_source_line} ...', "
|
|
164
|
+
f"globs={self.globs!r}, annotations={self.annotations!r})"
|
|
165
|
+
)
|
|
143
166
|
|
|
144
167
|
def __eq__(self, other):
|
|
145
168
|
if self.__class__ is other.__class__:
|
|
146
|
-
return (
|
|
169
|
+
return (
|
|
170
|
+
self.source_code,
|
|
171
|
+
self.globs,
|
|
172
|
+
self.annotations,
|
|
173
|
+
) == (
|
|
174
|
+
other.source_code,
|
|
175
|
+
other.globs,
|
|
176
|
+
other.annotations,
|
|
177
|
+
)
|
|
147
178
|
return NotImplemented
|
|
148
179
|
|
|
149
180
|
|
|
@@ -199,6 +230,10 @@ class MethodMaker:
|
|
|
199
230
|
# descriptor. Don't try to rename.
|
|
200
231
|
pass
|
|
201
232
|
|
|
233
|
+
# Apply annotations
|
|
234
|
+
if gen.annotations is not None:
|
|
235
|
+
method.__annotations__ = gen.annotations
|
|
236
|
+
|
|
202
237
|
# Replace this descriptor on the class with the generated function
|
|
203
238
|
setattr(gen_cls, self.funcname, method)
|
|
204
239
|
|
|
@@ -494,6 +529,9 @@ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
|
|
|
494
529
|
"""
|
|
495
530
|
The main builder for class generation
|
|
496
531
|
|
|
532
|
+
If the GATHERED_DATA attribute exists on the class it will be used instead of
|
|
533
|
+
the provided gatherer.
|
|
534
|
+
|
|
497
535
|
:param cls: Class to be analysed and have methods generated
|
|
498
536
|
:param gatherer: Function to gather field information
|
|
499
537
|
:type gatherer: Callable[[type], tuple[dict[str, Field], dict[str, Any]]]
|
|
@@ -514,12 +552,25 @@ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
|
|
|
514
552
|
gatherer=gatherer,
|
|
515
553
|
methods=methods,
|
|
516
554
|
flags=flags,
|
|
555
|
+
fix_signature=fix_signature,
|
|
517
556
|
)
|
|
518
557
|
|
|
519
|
-
internals
|
|
558
|
+
# Get from the class dict to avoid getting an inherited internals dict
|
|
559
|
+
internals = cls.__dict__.get(INTERNALS_DICT, {})
|
|
520
560
|
setattr(cls, INTERNALS_DICT, internals)
|
|
521
561
|
|
|
522
|
-
|
|
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)
|
|
523
574
|
|
|
524
575
|
for name, value in modifications.items():
|
|
525
576
|
if value is NOTHING:
|
|
@@ -537,11 +588,10 @@ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
|
|
|
537
588
|
for c in reversed(mro):
|
|
538
589
|
try:
|
|
539
590
|
fields.update(get_fields(c, local=True))
|
|
540
|
-
except AttributeError:
|
|
591
|
+
except (AttributeError, KeyError):
|
|
541
592
|
pass
|
|
542
593
|
|
|
543
594
|
internals["fields"] = fields
|
|
544
|
-
internals["flags"] = flags if flags is not None else {}
|
|
545
595
|
|
|
546
596
|
# Assign all of the method generators
|
|
547
597
|
internal_methods = {}
|
|
@@ -555,6 +605,9 @@ def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
|
|
|
555
605
|
if fix_signature:
|
|
556
606
|
setattr(cls, "__signature__", signature_maker)
|
|
557
607
|
|
|
608
|
+
# Add attribute indicating build completed
|
|
609
|
+
internals["build_complete"] = True
|
|
610
|
+
|
|
558
611
|
return cls
|
|
559
612
|
|
|
560
613
|
|
|
@@ -583,7 +636,35 @@ class SlotMakerMeta(type):
|
|
|
583
636
|
|
|
584
637
|
Will not convert `ClassVar` hinted values.
|
|
585
638
|
"""
|
|
586
|
-
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
|
+
|
|
587
668
|
# This should only run if slots=True is declared
|
|
588
669
|
# and __slots__ have not already been defined
|
|
589
670
|
if slots and "__slots__" not in ns:
|
|
@@ -611,16 +692,16 @@ class SlotMakerMeta(type):
|
|
|
611
692
|
else:
|
|
612
693
|
ns[k] = v
|
|
613
694
|
|
|
614
|
-
|
|
695
|
+
slot_values = {}
|
|
615
696
|
fields = {}
|
|
616
697
|
|
|
617
698
|
for k, v in cls_fields.items():
|
|
618
|
-
|
|
699
|
+
slot_values[k] = v.doc
|
|
619
700
|
if k not in {"__weakref__", "__dict__"}:
|
|
620
701
|
fields[k] = v
|
|
621
702
|
|
|
622
703
|
# Place slots *after* everything else to be safe
|
|
623
|
-
ns["__slots__"] =
|
|
704
|
+
ns["__slots__"] = slot_values
|
|
624
705
|
|
|
625
706
|
# Place pre-gathered field data - modifications are already applied
|
|
626
707
|
modifications = {}
|
|
@@ -790,7 +871,7 @@ def _build_field():
|
|
|
790
871
|
"init": Field(default=True, doc=field_docs["init"]),
|
|
791
872
|
"repr": Field(default=True, doc=field_docs["repr"]),
|
|
792
873
|
"compare": Field(default=True, doc=field_docs["compare"]),
|
|
793
|
-
"kw_only": Field(default=False, doc=field_docs["kw_only"])
|
|
874
|
+
"kw_only": Field(default=False, doc=field_docs["kw_only"]),
|
|
794
875
|
}
|
|
795
876
|
modifications = {"__slots__": field_docs}
|
|
796
877
|
|
|
@@ -810,21 +891,6 @@ _build_field()
|
|
|
810
891
|
del _build_field
|
|
811
892
|
|
|
812
893
|
|
|
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
|
-
|
|
828
894
|
def make_slot_gatherer(field_type=Field):
|
|
829
895
|
"""
|
|
830
896
|
Create a new annotation gatherer that will work with `Field` instances
|
|
@@ -907,8 +973,10 @@ def make_annotation_gatherer(
|
|
|
907
973
|
"""
|
|
908
974
|
def field_annotation_gatherer(cls_or_ns):
|
|
909
975
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
976
|
+
cls = None
|
|
910
977
|
cls_dict = cls_or_ns
|
|
911
978
|
else:
|
|
979
|
+
cls = cls_or_ns
|
|
912
980
|
cls_dict = cls_or_ns.__dict__
|
|
913
981
|
|
|
914
982
|
# This should really be dict[str, field_type] but static analysis
|
|
@@ -916,7 +984,7 @@ def make_annotation_gatherer(
|
|
|
916
984
|
cls_fields: dict[str, Field] = {}
|
|
917
985
|
modifications = {}
|
|
918
986
|
|
|
919
|
-
cls_annotations = get_ns_annotations(cls_dict)
|
|
987
|
+
cls_annotations = get_ns_annotations(cls_dict, cls=cls)
|
|
920
988
|
|
|
921
989
|
kw_flag = False
|
|
922
990
|
|
|
@@ -925,7 +993,7 @@ def make_annotation_gatherer(
|
|
|
925
993
|
if is_classvar(v):
|
|
926
994
|
continue
|
|
927
995
|
|
|
928
|
-
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):
|
|
929
997
|
if kw_flag:
|
|
930
998
|
raise SyntaxError("KW_ONLY sentinel may only appear once.")
|
|
931
999
|
kw_flag = True
|
|
@@ -963,13 +1031,14 @@ def make_annotation_gatherer(
|
|
|
963
1031
|
def make_field_gatherer(
|
|
964
1032
|
field_type=Field,
|
|
965
1033
|
leave_default_values=False,
|
|
966
|
-
assign_types=True,
|
|
967
1034
|
):
|
|
968
1035
|
def field_attribute_gatherer(cls_or_ns):
|
|
969
1036
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
970
1037
|
cls_dict = cls_or_ns
|
|
1038
|
+
cls = None
|
|
971
1039
|
else:
|
|
972
1040
|
cls_dict = cls_or_ns.__dict__
|
|
1041
|
+
cls = cls_or_ns
|
|
973
1042
|
|
|
974
1043
|
cls_attributes = {
|
|
975
1044
|
k: v
|
|
@@ -977,11 +1046,6 @@ def make_field_gatherer(
|
|
|
977
1046
|
if isinstance(v, field_type)
|
|
978
1047
|
}
|
|
979
1048
|
|
|
980
|
-
if assign_types:
|
|
981
|
-
cls_annotations = get_ns_annotations(cls_dict)
|
|
982
|
-
else:
|
|
983
|
-
cls_annotations = {}
|
|
984
|
-
|
|
985
1049
|
cls_modifications = {}
|
|
986
1050
|
|
|
987
1051
|
for name in cls_attributes.keys():
|
|
@@ -991,9 +1055,6 @@ def make_field_gatherer(
|
|
|
991
1055
|
else:
|
|
992
1056
|
cls_modifications[name] = NOTHING
|
|
993
1057
|
|
|
994
|
-
if assign_types and (anno := cls_annotations.get(name, NOTHING)) is not NOTHING:
|
|
995
|
-
cls_attributes[name] = field_type.from_field(attrib, type=anno)
|
|
996
|
-
|
|
997
1058
|
return cls_attributes, cls_modifications
|
|
998
1059
|
return field_attribute_gatherer
|
|
999
1060
|
|
|
@@ -1001,7 +1062,6 @@ def make_field_gatherer(
|
|
|
1001
1062
|
def make_unified_gatherer(
|
|
1002
1063
|
field_type=Field,
|
|
1003
1064
|
leave_default_values=False,
|
|
1004
|
-
ignore_annotations=False,
|
|
1005
1065
|
):
|
|
1006
1066
|
"""
|
|
1007
1067
|
Create a gatherer that will work via first slots, then
|
|
@@ -1010,7 +1070,6 @@ def make_unified_gatherer(
|
|
|
1010
1070
|
|
|
1011
1071
|
:param field_type: The field class to use for gathering
|
|
1012
1072
|
:param leave_default_values: leave default values in place
|
|
1013
|
-
:param ignore_annotations: don't attempt to read annotations
|
|
1014
1073
|
:return: gatherer function
|
|
1015
1074
|
"""
|
|
1016
1075
|
slot_g = make_slot_gatherer(field_type)
|
|
@@ -1020,25 +1079,26 @@ def make_unified_gatherer(
|
|
|
1020
1079
|
def field_unified_gatherer(cls_or_ns):
|
|
1021
1080
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
1022
1081
|
cls_dict = cls_or_ns
|
|
1082
|
+
cls = None
|
|
1023
1083
|
else:
|
|
1024
1084
|
cls_dict = cls_or_ns.__dict__
|
|
1025
|
-
|
|
1026
|
-
cls_gathered = cls_dict.get(GATHERED_DATA)
|
|
1027
|
-
if cls_gathered:
|
|
1028
|
-
return pre_gathered_gatherer(cls_dict)
|
|
1085
|
+
cls = cls_or_ns
|
|
1029
1086
|
|
|
1030
1087
|
cls_slots = cls_dict.get("__slots__")
|
|
1031
1088
|
|
|
1032
1089
|
if isinstance(cls_slots, SlotFields):
|
|
1033
1090
|
return slot_g(cls_dict)
|
|
1034
1091
|
|
|
1092
|
+
# Get ignore_annotations flag
|
|
1093
|
+
ignore_annotations = cls_dict.get(INTERNALS_DICT, {}).get("flags", {}).get("ignore_annotations", False)
|
|
1094
|
+
|
|
1035
1095
|
if ignore_annotations:
|
|
1036
1096
|
return attrib_g(cls_dict)
|
|
1037
1097
|
else:
|
|
1038
1098
|
# To choose between annotation and attribute gatherers
|
|
1039
1099
|
# compare sets of names.
|
|
1040
1100
|
# Don't bother evaluating string annotations, as we only need names
|
|
1041
|
-
cls_annotations = get_ns_annotations(cls_dict)
|
|
1101
|
+
cls_annotations = get_ns_annotations(cls_dict, cls=cls)
|
|
1042
1102
|
cls_attributes = {
|
|
1043
1103
|
k: v for k, v in cls_dict.items() if isinstance(v, field_type)
|
|
1044
1104
|
}
|
|
@@ -1048,7 +1108,9 @@ def make_unified_gatherer(
|
|
|
1048
1108
|
|
|
1049
1109
|
if set(cls_annotation_names).issuperset(set(cls_attribute_names)):
|
|
1050
1110
|
# All `Field` values have annotations, so use annotation gatherer
|
|
1051
|
-
|
|
1111
|
+
# Pass the original cls_or_ns object
|
|
1112
|
+
|
|
1113
|
+
return anno_g(cls_or_ns)
|
|
1052
1114
|
|
|
1053
1115
|
return attrib_g(cls_dict)
|
|
1054
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:
|
|
@@ -40,16 +48,20 @@ class KW_ONLY(metaclass=_KW_ONLY_META): ...
|
|
|
40
48
|
class _CodegenType(typing.Protocol):
|
|
41
49
|
def __call__(self, cls: type, funcname: str = ...) -> GeneratedCode: ...
|
|
42
50
|
|
|
43
|
-
|
|
44
51
|
class GeneratedCode:
|
|
45
|
-
__slots__: tuple[str,
|
|
52
|
+
__slots__: tuple[str, ...]
|
|
46
53
|
source_code: str
|
|
47
54
|
globs: dict[str, typing.Any]
|
|
55
|
+
annotations: dict[str, typing.Any]
|
|
48
56
|
|
|
49
|
-
def __init__(
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
source_code: str,
|
|
60
|
+
globs: dict[str, typing.Any],
|
|
61
|
+
annotations: dict[str, typing.Any] | None = ...,
|
|
62
|
+
) -> None: ...
|
|
50
63
|
def __repr__(self) -> str: ...
|
|
51
64
|
|
|
52
|
-
|
|
53
65
|
class MethodMaker:
|
|
54
66
|
funcname: str
|
|
55
67
|
code_generator: _CodegenType
|
|
@@ -126,6 +138,7 @@ class SlotMakerMeta(type):
|
|
|
126
138
|
ns: dict[str, typing.Any],
|
|
127
139
|
slots: bool = ...,
|
|
128
140
|
gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]] | None = ...,
|
|
141
|
+
ignore_annotations: bool | None = ...,
|
|
129
142
|
**kwargs: typing.Any,
|
|
130
143
|
) -> _T: ...
|
|
131
144
|
|
|
@@ -164,16 +177,11 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
164
177
|
@classmethod
|
|
165
178
|
def from_field(cls, fld: Field, /, **kwargs: typing.Any) -> Field: ...
|
|
166
179
|
|
|
167
|
-
|
|
168
180
|
# type[Field] doesn't work due to metaclass
|
|
169
181
|
# This is not really precise enough because isinstance is used
|
|
170
182
|
_ReturnsField = Callable[..., Field]
|
|
171
183
|
_FieldType = typing.TypeVar("_FieldType", bound=Field)
|
|
172
184
|
|
|
173
|
-
def pre_gathered_gatherer(
|
|
174
|
-
cls_or_ns: type | _CopiableMappings
|
|
175
|
-
) -> tuple[dict[str, Field | _FieldType], dict[str, typing.Any]]: ...
|
|
176
|
-
|
|
177
185
|
@typing.overload
|
|
178
186
|
def make_slot_gatherer(
|
|
179
187
|
field_type: type[_FieldType]
|
|
@@ -200,7 +208,6 @@ def make_annotation_gatherer(
|
|
|
200
208
|
def make_field_gatherer(
|
|
201
209
|
field_type: type[_FieldType],
|
|
202
210
|
leave_default_values: bool = False,
|
|
203
|
-
assign_types: bool = True,
|
|
204
211
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
|
|
205
212
|
|
|
206
213
|
@typing.overload
|
|
@@ -213,14 +220,12 @@ def make_field_gatherer(
|
|
|
213
220
|
def make_unified_gatherer(
|
|
214
221
|
field_type: type[_FieldType],
|
|
215
222
|
leave_default_values: bool = ...,
|
|
216
|
-
ignore_annotations: bool = ...,
|
|
217
223
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
|
|
218
224
|
|
|
219
225
|
@typing.overload
|
|
220
226
|
def make_unified_gatherer(
|
|
221
227
|
field_type: _ReturnsField = ...,
|
|
222
228
|
leave_default_values: bool = ...,
|
|
223
|
-
ignore_annotations: bool = ...,
|
|
224
229
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
|
|
225
230
|
|
|
226
231
|
|
|
@@ -259,9 +264,6 @@ class GatheredFields:
|
|
|
259
264
|
fields: dict[str, Field]
|
|
260
265
|
modifications: dict[str, typing.Any]
|
|
261
266
|
|
|
262
|
-
__classbuilder_internals__: dict
|
|
263
|
-
__signature__: inspect.Signature
|
|
264
|
-
|
|
265
267
|
def __init__(
|
|
266
268
|
self,
|
|
267
269
|
fields: dict[str, Field],
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "0.
|
|
2
|
-
__version_tuple__ = (0,
|
|
1
|
+
__version__ = "0.11.0"
|
|
2
|
+
__version_tuple__ = (0, 11, 0)
|
|
@@ -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
|
|
@@ -19,89 +19,86 @@
|
|
|
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
|
-
import sys
|
|
23
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
|
+
"""
|
|
24
33
|
|
|
25
34
|
class _LazyAnnotationLib:
|
|
26
35
|
def __getattr__(self, item):
|
|
27
|
-
global
|
|
36
|
+
global _lazy_annotationlib
|
|
28
37
|
import annotationlib # type: ignore
|
|
29
|
-
|
|
38
|
+
_lazy_annotationlib = annotationlib
|
|
30
39
|
return getattr(annotationlib, item)
|
|
31
40
|
|
|
32
41
|
|
|
33
42
|
_lazy_annotationlib = _LazyAnnotationLib()
|
|
34
43
|
|
|
35
44
|
|
|
36
|
-
def get_func_annotations(func):
|
|
45
|
+
def get_func_annotations(func, use_forwardref=False):
|
|
37
46
|
"""
|
|
38
47
|
Given a function, return the annotations dictionary
|
|
39
48
|
|
|
40
49
|
:param func: function object
|
|
41
50
|
:return: dictionary of annotations
|
|
42
51
|
"""
|
|
43
|
-
#
|
|
44
|
-
# the __prefab_post_init__ function
|
|
52
|
+
# Try to get `__annotations__` for VALUE annotations first
|
|
45
53
|
try:
|
|
46
|
-
|
|
54
|
+
raw_annotations = func.__annotations__
|
|
47
55
|
except Exception:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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()
|
|
55
64
|
|
|
56
65
|
return annotations
|
|
57
66
|
|
|
58
67
|
|
|
59
|
-
def get_ns_annotations(ns):
|
|
68
|
+
def get_ns_annotations(ns, cls=None, use_forwardref=False):
|
|
60
69
|
"""
|
|
61
70
|
Given a class namespace, attempt to retrieve the
|
|
62
71
|
annotations dictionary.
|
|
63
72
|
|
|
64
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
|
|
65
76
|
:return: dictionary of annotations
|
|
66
77
|
"""
|
|
67
78
|
|
|
68
79
|
annotations = ns.get("__annotations__")
|
|
69
80
|
if annotations is not None:
|
|
70
81
|
annotations = annotations.copy()
|
|
71
|
-
|
|
82
|
+
else:
|
|
72
83
|
# See if we're using PEP-649 annotations
|
|
73
84
|
annotate = _lazy_annotationlib.get_annotate_from_class_namespace(ns)
|
|
74
85
|
if annotate:
|
|
75
|
-
|
|
76
|
-
annotate
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
)
|
|
79
100
|
|
|
80
101
|
if annotations is None:
|
|
81
102
|
annotations = {}
|
|
82
103
|
|
|
83
104
|
return annotations
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def is_classvar(hint):
|
|
87
|
-
if isinstance(hint, str):
|
|
88
|
-
# String annotations, just check if the string 'ClassVar' is in there
|
|
89
|
-
# This is overly broad and could be smarter.
|
|
90
|
-
return "ClassVar" in hint
|
|
91
|
-
elif (annotationlib := sys.modules.get("annotationlib")) and isinstance(hint, annotationlib.ForwardRef):
|
|
92
|
-
return "ClassVar" in hint.__arg__
|
|
93
|
-
else:
|
|
94
|
-
_typing = sys.modules.get("typing")
|
|
95
|
-
if _typing:
|
|
96
|
-
_Annotated = _typing.Annotated
|
|
97
|
-
_get_origin = _typing.get_origin
|
|
98
|
-
|
|
99
|
-
if _Annotated and _get_origin(hint) is _Annotated:
|
|
100
|
-
hint = getattr(hint, "__origin__", None)
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
hint is _typing.ClassVar
|
|
104
|
-
or getattr(hint, "__origin__", None) is _typing.ClassVar
|
|
105
|
-
):
|
|
106
|
-
return True
|
|
107
|
-
return False
|
|
@@ -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,14 +1,19 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
import typing
|
|
2
3
|
import types
|
|
4
|
+
import sys
|
|
3
5
|
|
|
4
6
|
_CopiableMappings = dict[str, typing.Any] | types.MappingProxyType[str, typing.Any]
|
|
5
7
|
|
|
6
8
|
def get_func_annotations(
|
|
7
9
|
func: types.FunctionType,
|
|
10
|
+
use_forwardref: bool = ...,
|
|
8
11
|
) -> dict[str, typing.Any]: ...
|
|
9
12
|
|
|
10
13
|
def get_ns_annotations(
|
|
11
14
|
ns: _CopiableMappings,
|
|
15
|
+
cls: type | None = ...,
|
|
16
|
+
use_forwardref: bool = ...,
|
|
12
17
|
) -> dict[str, typing.Any]: ...
|
|
13
18
|
|
|
14
19
|
def is_classvar(
|
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__"
|
|
@@ -64,6 +64,7 @@ def get_attributes(cls):
|
|
|
64
64
|
# Method Generators
|
|
65
65
|
def init_generator(cls, funcname="__init__"):
|
|
66
66
|
globs = {}
|
|
67
|
+
annotations = {}
|
|
67
68
|
# Get the internals dictionary and prepare attributes
|
|
68
69
|
attributes = get_attributes(cls)
|
|
69
70
|
flags = get_flags(cls)
|
|
@@ -106,41 +107,29 @@ def init_generator(cls, funcname="__init__"):
|
|
|
106
107
|
kw_only_arglist = []
|
|
107
108
|
for name, attrib in attributes.items():
|
|
108
109
|
# post_init annotations can be used to broaden types.
|
|
109
|
-
if name in post_init_annotations:
|
|
110
|
-
globs[f"_{name}_type"] = post_init_annotations[name]
|
|
111
|
-
elif attrib.type is not NOTHING:
|
|
112
|
-
globs[f"_{name}_type"] = attrib.type
|
|
113
|
-
|
|
114
110
|
if attrib.init:
|
|
111
|
+
if name in post_init_annotations:
|
|
112
|
+
annotations[name] = post_init_annotations[name]
|
|
113
|
+
elif attrib.type is not NOTHING:
|
|
114
|
+
annotations[name] = attrib.type
|
|
115
|
+
|
|
115
116
|
if attrib.default is not NOTHING:
|
|
116
117
|
if isinstance(attrib.default, (str, int, float, bool)):
|
|
117
118
|
# Just use the literal in these cases
|
|
118
|
-
|
|
119
|
-
arg = f"{name}={attrib.default!r}"
|
|
120
|
-
else:
|
|
121
|
-
arg = f"{name}: _{name}_type = {attrib.default!r}"
|
|
119
|
+
arg = f"{name}={attrib.default!r}"
|
|
122
120
|
else:
|
|
123
121
|
# No guarantee repr will work for other objects
|
|
124
122
|
# so store the value in a variable and put it
|
|
125
123
|
# in the globals dict for eval
|
|
126
|
-
|
|
127
|
-
arg = f"{name}=_{name}_default"
|
|
128
|
-
else:
|
|
129
|
-
arg = f"{name}: _{name}_type = _{name}_default"
|
|
124
|
+
arg = f"{name}=_{name}_default"
|
|
130
125
|
globs[f"_{name}_default"] = attrib.default
|
|
131
126
|
elif attrib.default_factory is not NOTHING:
|
|
132
127
|
# Use NONE here and call the factory later
|
|
133
128
|
# This matches the behaviour of compiled
|
|
134
|
-
|
|
135
|
-
arg = f"{name}=None"
|
|
136
|
-
else:
|
|
137
|
-
arg = f"{name}: _{name}_type = None"
|
|
129
|
+
arg = f"{name}=None"
|
|
138
130
|
globs[f"_{name}_factory"] = attrib.default_factory
|
|
139
131
|
else:
|
|
140
|
-
|
|
141
|
-
arg = name
|
|
142
|
-
else:
|
|
143
|
-
arg = f"{name}: _{name}_type"
|
|
132
|
+
arg = name
|
|
144
133
|
if attrib.kw_only or kw_only:
|
|
145
134
|
kw_only_arglist.append(arg)
|
|
146
135
|
else:
|
|
@@ -206,13 +195,19 @@ def init_generator(cls, funcname="__init__"):
|
|
|
206
195
|
post_init_call = ""
|
|
207
196
|
|
|
208
197
|
code = (
|
|
209
|
-
f"def {funcname}(self, {args})
|
|
198
|
+
f"def {funcname}(self, {args}):\n"
|
|
210
199
|
f"{pre_init_call}\n"
|
|
211
200
|
f"{body}\n"
|
|
212
201
|
f"{post_init_call}\n"
|
|
213
202
|
)
|
|
214
203
|
|
|
215
|
-
|
|
204
|
+
if annotations:
|
|
205
|
+
annotations["return"] = None
|
|
206
|
+
else:
|
|
207
|
+
# If there are no annotations, return an unannotated init function
|
|
208
|
+
annotations = None
|
|
209
|
+
|
|
210
|
+
return GeneratedCode(code, globs, annotations)
|
|
216
211
|
|
|
217
212
|
|
|
218
213
|
def iter_generator(cls, funcname="__iter__"):
|
|
@@ -332,7 +327,7 @@ def attribute(
|
|
|
332
327
|
:param private: Short for init, repr, compare, iter, serialize = False, must have default or factory
|
|
333
328
|
:param doc: Parameter documentation for slotted classes
|
|
334
329
|
:param metadata: Dictionary for additional non-construction metadata
|
|
335
|
-
:param type: Type of this attribute
|
|
330
|
+
:param type: Type of this attribute
|
|
336
331
|
|
|
337
332
|
:return: Attribute generated with these parameters.
|
|
338
333
|
"""
|
|
@@ -366,7 +361,10 @@ def attribute(
|
|
|
366
361
|
)
|
|
367
362
|
|
|
368
363
|
|
|
369
|
-
prefab_gatherer = make_unified_gatherer(
|
|
364
|
+
prefab_gatherer = make_unified_gatherer(
|
|
365
|
+
Attribute,
|
|
366
|
+
leave_default_values=False,
|
|
367
|
+
)
|
|
370
368
|
|
|
371
369
|
|
|
372
370
|
# Class Builders
|
|
@@ -381,9 +379,11 @@ def _make_prefab(
|
|
|
381
379
|
match_args=True,
|
|
382
380
|
kw_only=False,
|
|
383
381
|
frozen=False,
|
|
382
|
+
replace=True,
|
|
384
383
|
dict_method=False,
|
|
385
384
|
recursive_repr=False,
|
|
386
385
|
gathered_fields=None,
|
|
386
|
+
ignore_annotations=False,
|
|
387
387
|
):
|
|
388
388
|
"""
|
|
389
389
|
Generate boilerplate code for dunder methods in a class.
|
|
@@ -398,21 +398,22 @@ def _make_prefab(
|
|
|
398
398
|
:param frozen: Prevent attribute values from being changed once defined
|
|
399
399
|
(This does not prevent the modification of mutable attributes
|
|
400
400
|
such as lists)
|
|
401
|
+
:param replace: Add a generated __replace__ method
|
|
401
402
|
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
402
403
|
:param recursive_repr: Safely handle repr in case of recursion
|
|
403
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
|
|
404
406
|
:return: class with __ methods defined
|
|
405
407
|
"""
|
|
406
408
|
cls_dict = cls.__dict__
|
|
407
409
|
|
|
408
|
-
if
|
|
410
|
+
if build_completed(cls_dict):
|
|
409
411
|
raise PrefabError(
|
|
410
412
|
f"Decorated class {cls.__name__!r} "
|
|
411
413
|
f"has already been processed as a Prefab."
|
|
412
414
|
)
|
|
413
415
|
|
|
414
416
|
slots = cls_dict.get("__slots__")
|
|
415
|
-
|
|
416
417
|
slotted = False if slots is None else True
|
|
417
418
|
|
|
418
419
|
if gathered_fields is None:
|
|
@@ -443,11 +444,22 @@ def _make_prefab(
|
|
|
443
444
|
if dict_method:
|
|
444
445
|
methods.add(asdict_maker)
|
|
445
446
|
|
|
446
|
-
|
|
447
|
+
if replace and "__replace__" not in cls_dict:
|
|
448
|
+
methods.add(replace_maker)
|
|
447
449
|
|
|
448
450
|
flags = {
|
|
449
|
-
"kw_only": kw_only,
|
|
450
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,
|
|
451
463
|
}
|
|
452
464
|
|
|
453
465
|
cls = builder(
|
|
@@ -556,30 +568,71 @@ def _make_prefab(
|
|
|
556
568
|
class Prefab(metaclass=SlotMakerMeta, gatherer=prefab_gatherer):
|
|
557
569
|
__slots__ = {} # type: ignore
|
|
558
570
|
|
|
559
|
-
# noinspection PyShadowingBuiltins
|
|
560
571
|
def __init_subclass__(
|
|
561
572
|
cls,
|
|
562
|
-
|
|
563
|
-
repr=True,
|
|
564
|
-
eq=True,
|
|
565
|
-
iter=False,
|
|
566
|
-
match_args=True,
|
|
567
|
-
kw_only=False,
|
|
568
|
-
frozen=False,
|
|
569
|
-
dict_method=False,
|
|
570
|
-
recursive_repr=False,
|
|
573
|
+
**kwargs
|
|
571
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
|
+
|
|
631
|
+
print(flags)
|
|
632
|
+
|
|
572
633
|
_make_prefab(
|
|
573
634
|
cls,
|
|
574
|
-
|
|
575
|
-
repr=repr,
|
|
576
|
-
eq=eq,
|
|
577
|
-
iter=iter,
|
|
578
|
-
match_args=match_args,
|
|
579
|
-
kw_only=kw_only,
|
|
580
|
-
frozen=frozen,
|
|
581
|
-
dict_method=dict_method,
|
|
582
|
-
recursive_repr=recursive_repr,
|
|
635
|
+
**flags
|
|
583
636
|
)
|
|
584
637
|
|
|
585
638
|
|
|
@@ -594,8 +647,10 @@ def prefab(
|
|
|
594
647
|
match_args=True,
|
|
595
648
|
kw_only=False,
|
|
596
649
|
frozen=False,
|
|
650
|
+
replace=True,
|
|
597
651
|
dict_method=False,
|
|
598
652
|
recursive_repr=False,
|
|
653
|
+
ignore_annotations=False,
|
|
599
654
|
):
|
|
600
655
|
"""
|
|
601
656
|
Generate boilerplate code for dunder methods in a class.
|
|
@@ -611,8 +666,11 @@ def prefab(
|
|
|
611
666
|
:param kw_only: make all attributes keyword only
|
|
612
667
|
:param frozen: Prevent attribute values from being changed once defined
|
|
613
668
|
(This does not prevent the modification of mutable attributes such as lists)
|
|
669
|
+
:param replace: generate a __replace__ method
|
|
614
670
|
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
615
671
|
:param recursive_repr: Safely handle repr in case of recursion
|
|
672
|
+
:param ignore_annotations: Ignore type annotations when gathering fields, only look for
|
|
673
|
+
slots or attribute(...) values
|
|
616
674
|
|
|
617
675
|
:return: class with __ methods defined
|
|
618
676
|
"""
|
|
@@ -627,8 +685,10 @@ def prefab(
|
|
|
627
685
|
match_args=match_args,
|
|
628
686
|
kw_only=kw_only,
|
|
629
687
|
frozen=frozen,
|
|
688
|
+
replace=replace,
|
|
630
689
|
dict_method=dict_method,
|
|
631
690
|
recursive_repr=recursive_repr,
|
|
691
|
+
ignore_annotations=ignore_annotations,
|
|
632
692
|
)
|
|
633
693
|
else:
|
|
634
694
|
return _make_prefab(
|
|
@@ -640,8 +700,10 @@ def prefab(
|
|
|
640
700
|
match_args=match_args,
|
|
641
701
|
kw_only=kw_only,
|
|
642
702
|
frozen=frozen,
|
|
703
|
+
replace=replace,
|
|
643
704
|
dict_method=dict_method,
|
|
644
705
|
recursive_repr=recursive_repr,
|
|
706
|
+
ignore_annotations=ignore_annotations,
|
|
645
707
|
)
|
|
646
708
|
|
|
647
709
|
|
|
@@ -659,6 +721,7 @@ def build_prefab(
|
|
|
659
721
|
match_args=True,
|
|
660
722
|
kw_only=False,
|
|
661
723
|
frozen=False,
|
|
724
|
+
replace=True,
|
|
662
725
|
dict_method=False,
|
|
663
726
|
recursive_repr=False,
|
|
664
727
|
slots=False,
|
|
@@ -680,6 +743,7 @@ def build_prefab(
|
|
|
680
743
|
:param kw_only: make all attributes keyword only
|
|
681
744
|
:param frozen: Prevent attribute values from being changed once defined
|
|
682
745
|
(This does not prevent the modification of mutable attributes such as lists)
|
|
746
|
+
:param replace: generate a __replace__ method
|
|
683
747
|
:param dict_method: Include an as_dict method for faster dictionary creation
|
|
684
748
|
:param recursive_repr: Safely handle repr in case of recursion
|
|
685
749
|
:param slots: Make the resulting class slotted
|
|
@@ -708,6 +772,7 @@ def build_prefab(
|
|
|
708
772
|
class_dict["__slots__"] = class_slots
|
|
709
773
|
|
|
710
774
|
class_dict["__annotations__"] = class_annotations
|
|
775
|
+
|
|
711
776
|
cls = type(class_name, bases, class_dict)
|
|
712
777
|
|
|
713
778
|
gathered_fields = GatheredFields(fields, {})
|
|
@@ -721,6 +786,7 @@ def build_prefab(
|
|
|
721
786
|
match_args=match_args,
|
|
722
787
|
kw_only=kw_only,
|
|
723
788
|
frozen=frozen,
|
|
789
|
+
replace=replace,
|
|
724
790
|
dict_method=dict_method,
|
|
725
791
|
recursive_repr=recursive_repr,
|
|
726
792
|
gathered_fields=gathered_fields,
|
|
@@ -784,6 +850,17 @@ def as_dict(o):
|
|
|
784
850
|
}
|
|
785
851
|
|
|
786
852
|
def replace(obj, /, **changes):
|
|
853
|
+
"""
|
|
854
|
+
Create a copy of a prefab instance with values provided to 'changes' replaced
|
|
855
|
+
|
|
856
|
+
:param obj: prefab instance
|
|
857
|
+
:return: new prefab instance
|
|
858
|
+
"""
|
|
787
859
|
if not is_prefab_instance(obj):
|
|
788
860
|
raise TypeError("replace() should be called on prefab instances")
|
|
789
|
-
|
|
861
|
+
try:
|
|
862
|
+
replace_func = obj.__replace__
|
|
863
|
+
except AttributeError:
|
|
864
|
+
raise TypeError(f"{obj.__class__.__name__!r} does not support __replace__")
|
|
865
|
+
|
|
866
|
+
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.1.dist-info → ducktools_classbuilder-0.11.0.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.0
|
|
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=XCxV77c5KfBMiOfJNfOT2ca4mIptAMbl0__7iQH-pMY,54
|
|
4
|
+
ducktools/classbuilder/annotations.pyi,sha256=bKTwQlPydbwrbVGaDu_PoSYOhuaqv8I_tMHf-g4aT0M,476
|
|
5
|
+
ducktools/classbuilder/prefab.py,sha256=KoRvTZyW7hAS0w8pzRho3FzRvzE9ztuWutHbKOscB_0,27391
|
|
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.0.dist-info/licenses/LICENSE,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
12
|
+
ducktools_classbuilder-0.11.0.dist-info/METADATA,sha256=WnqrTjsS__wBR6uCDIqVPjairFcyjj_zP8KwwrCLCgw,9204
|
|
13
|
+
ducktools_classbuilder-0.11.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
ducktools_classbuilder-0.11.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
15
|
+
ducktools_classbuilder-0.11.0.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
{ducktools_classbuilder-0.10.1.dist-info → ducktools_classbuilder-0.11.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.10.1.dist-info → ducktools_classbuilder-0.11.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|