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.

@@ -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 f"GeneratorOutput(source_code='{first_source_line} ...', globs={self.globs!r})"
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 (self.source_code, self.globs) == (other.source_code, other.globs)
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
- cls_fields, modifications = gatherer(cls)
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__(cls, name, bases, ns, slots=True, gatherer=None, **kwargs):
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
- slots = {}
695
+ slot_values = {}
615
696
  fields = {}
616
697
 
617
698
  for k, v in cls_fields.items():
618
- slots[k] = v.doc
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__"] = 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 v == "KW_ONLY"):
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
- return anno_g(cls_dict)
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
- _py_type = type | str # Alias for type hint values
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, 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__(self, source_code: str, globs: dict[str, typing.Any]) -> None: ...
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.10.1"
2
- __version_tuple__ = (0, 10, 1)
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 _lazyannotationlib
36
+ global _lazy_annotationlib
28
37
  import annotationlib # type: ignore
29
- _lazyannotationlib = annotationlib
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
- # This method exists for use by prefab in getting annotations from
44
- # the __prefab_post_init__ function
52
+ # Try to get `__annotations__` for VALUE annotations first
45
53
  try:
46
- annotations = func.__annotations__
54
+ raw_annotations = func.__annotations__
47
55
  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
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
- elif sys.version_info >= (3, 14):
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
- annotations = _lazy_annotationlib.call_annotate_function(
76
- annotate,
77
- format=_lazy_annotationlib.Format.FORWARDREF
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(
@@ -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
- # noinspection PyUnresolvedReferences
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
- if attrib.type is NOTHING:
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
- if attrib.type is NOTHING:
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
- if attrib.type is NOTHING:
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
- if attrib.type is NOTHING:
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}) -> None:\n"
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
- return GeneratedCode(code, globs)
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 (for slotted classes)
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(Attribute, False)
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 INTERNALS_DICT in cls_dict:
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
- methods.add(replace_maker)
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
- init=True,
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
- init=init,
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
- return obj.__replace__(**changes)
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,
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ducktools-classbuilder
3
- Version: 0.10.1
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,,