ducktools-classbuilder 0.10.2__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.

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