ducktools-classbuilder 0.10.0__py3-none-any.whl → 0.10.2__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,8 +31,9 @@
31
31
  # Field itself sidesteps this by defining __slots__ to avoid that branch.
32
32
 
33
33
  import os
34
+ import sys
34
35
 
35
- from .annotations import get_ns_annotations, is_classvar
36
+ from .annotations import get_ns_annotations, is_classvar, make_annotate_func
36
37
  from ._version import __version__, __version_tuple__ # noqa: F401
37
38
 
38
39
  # Change this name if you make heavy modifications
@@ -131,19 +132,25 @@ class GeneratedCode:
131
132
  This class provides a return value for the generated output from source code
132
133
  generators.
133
134
  """
134
- __slots__ = ("source_code", "globs")
135
+ __slots__ = ("source_code", "globs", "annotations")
135
136
 
136
- def __init__(self, source_code, globs):
137
+ def __init__(self, source_code, globs, annotations=None):
137
138
  self.source_code = source_code
138
139
  self.globs = globs
140
+ self.annotations = annotations
139
141
 
140
142
  def __repr__(self):
141
143
  first_source_line = self.source_code.split("\n")[0]
142
- return f"GeneratorOutput(source_code='{first_source_line} ...', globs={self.globs!r})"
144
+ return (
145
+ f"GeneratorOutput(source_code='{first_source_line} ...', "
146
+ f"globs={self.globs!r}, annotations={self.annotations!r})"
147
+ )
143
148
 
144
149
  def __eq__(self, other):
145
150
  if self.__class__ is other.__class__:
146
- return (self.source_code, self.globs) == (other.source_code, other.globs)
151
+ return (self.source_code, self.globs, self.annotations) == (
152
+ other.source_code, other.globs, other.annotations
153
+ )
147
154
  return NotImplemented
148
155
 
149
156
 
@@ -199,6 +206,20 @@ class MethodMaker:
199
206
  # descriptor. Don't try to rename.
200
207
  pass
201
208
 
209
+ # Apply annotations
210
+ 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
222
+
202
223
  # Replace this descriptor on the class with the generated function
203
224
  setattr(gen_cls, self.funcname, method)
204
225
 
@@ -217,15 +238,23 @@ class _SignatureMaker:
217
238
  # help(cls) will fail along with inspect.signature(cls)
218
239
  # This signature maker descriptor is placed to override __signature__ and force
219
240
  # the `__init__` signature to be generated first if the signature is requested.
220
- def __get__(self, instance, cls):
221
- import inspect # Deferred inspect import
222
- _ = cls.__init__ # force generation of `__init__` function
223
- # Remove this attribute from the class
224
- # This prevents recursion back into this __get__ method.
225
- delattr(cls, "__signature__")
226
- sig = inspect.signature(cls)
227
- setattr(cls, "__signature__", sig)
228
- return sig
241
+ def __get__(self, instance, cls=None):
242
+ if cls is None:
243
+ cls = type(instance)
244
+
245
+ # force generation of `__init__` function
246
+ _ = cls.__init__
247
+
248
+ if instance is None:
249
+ raise AttributeError(
250
+ f"type object {cls.__name__!r} "
251
+ "has no attribute '__signature__'"
252
+ )
253
+ else:
254
+ raise AttributeError(
255
+ f"{cls.__name__!r} object"
256
+ "has no attribute '__signature__'"
257
+ )
229
258
 
230
259
 
231
260
  signature_maker = _SignatureMaker()
@@ -393,7 +422,7 @@ def replace_generator(cls, funcname="__replace__"):
393
422
  # Generate the replace method for built classes
394
423
  # unlike the dataclasses implementation this is generated
395
424
  attribs = get_fields(cls)
396
-
425
+
397
426
  # This is essentially the as_dict generator for prefabs
398
427
  # except based on attrib.init instead of .serialize
399
428
  vals = ", ".join(
@@ -407,7 +436,7 @@ def replace_generator(cls, funcname="__replace__"):
407
436
  f"def {funcname}(self, /, **changes):\n"
408
437
  f" new_kwargs = {init_dict}\n"
409
438
  f" for name, value in changes.items():\n"
410
- f" if name not in new_kwargs:\n"
439
+ f" if name not in new_kwargs:\n"
411
440
  f" raise TypeError(\n"
412
441
  f" f\"{{name!r}} is not a valid replacable \"\n"
413
442
  f" f\"field on {{self.__class__.__name__!r}}\"\n"
@@ -578,7 +607,7 @@ class SlotMakerMeta(type):
578
607
  def __new__(cls, name, bases, ns, slots=True, gatherer=None, **kwargs):
579
608
  # This should only run if slots=True is declared
580
609
  # and __slots__ have not already been defined
581
- if slots and "__slots__" not in ns:
610
+ if slots and "__slots__" not in ns:
582
611
  # Check if a different gatherer has been set in any base classes
583
612
  # Default to unified gatherer
584
613
  if gatherer is None:
@@ -617,7 +646,7 @@ class SlotMakerMeta(type):
617
646
  # Place pre-gathered field data - modifications are already applied
618
647
  modifications = {}
619
648
  ns[GATHERED_DATA] = fields, modifications
620
-
649
+
621
650
  else:
622
651
  if gatherer is not None:
623
652
  ns[META_GATHERER_NAME] = gatherer
@@ -642,7 +671,7 @@ class GatheredFields:
642
671
  def __eq__(self, other):
643
672
  if type(self) is type(other):
644
673
  return self.fields == other.fields and self.modifications == other.modifications
645
-
674
+
646
675
  def __repr__(self):
647
676
  return f"{type(self).__name__}(fields={self.fields!r}, modifications={self.modifications!r})"
648
677
 
@@ -761,11 +790,11 @@ def _build_field():
761
790
  # Complete the construction of the Field class
762
791
  field_docs = {
763
792
  "default": "Standard default value to be used for attributes with this field.",
764
- "default_factory":
793
+ "default_factory":
765
794
  "A zero-argument function to be called to generate a default value, "
766
795
  "useful for mutable obects like lists.",
767
796
  "type": "The type of the attribute to be assigned by this field.",
768
- "doc":
797
+ "doc":
769
798
  "The documentation for the attribute that appears when calling "
770
799
  "help(...) on the class. (Only in slotted classes).",
771
800
  "init": "Include this attribute in the class __init__ parameters.",
@@ -813,7 +842,7 @@ def pre_gathered_gatherer(cls_or_ns):
813
842
  cls_dict = cls_or_ns
814
843
  else:
815
844
  cls_dict = cls_or_ns.__dict__
816
-
845
+
817
846
  return cls_dict[GATHERED_DATA]
818
847
 
819
848
 
@@ -1043,7 +1072,7 @@ def make_unified_gatherer(
1043
1072
  return anno_g(cls_dict)
1044
1073
 
1045
1074
  return attrib_g(cls_dict)
1046
-
1075
+
1047
1076
  return field_unified_gatherer
1048
1077
 
1049
1078
 
@@ -1,5 +1,6 @@
1
1
  import types
2
2
  import typing
3
+ import typing_extensions
3
4
 
4
5
  import inspect
5
6
 
@@ -39,16 +40,20 @@ class KW_ONLY(metaclass=_KW_ONLY_META): ...
39
40
  class _CodegenType(typing.Protocol):
40
41
  def __call__(self, cls: type, funcname: str = ...) -> GeneratedCode: ...
41
42
 
42
-
43
43
  class GeneratedCode:
44
- __slots__: tuple[str, str]
44
+ __slots__: tuple[str, ...]
45
45
  source_code: str
46
46
  globs: dict[str, typing.Any]
47
+ annotations: dict[str, typing.Any]
47
48
 
48
- def __init__(self, source_code: str, globs: dict[str, typing.Any]) -> None: ...
49
+ def __init__(
50
+ self,
51
+ source_code: str,
52
+ globs: dict[str, typing.Any],
53
+ annotations: dict[str, typing.Any] | None = ...,
54
+ ) -> None: ...
49
55
  def __repr__(self) -> str: ...
50
56
 
51
-
52
57
  class MethodMaker:
53
58
  funcname: str
54
59
  code_generator: _CodegenType
@@ -57,7 +62,7 @@ class MethodMaker:
57
62
  def __get__(self, instance, cls) -> Callable: ...
58
63
 
59
64
  class _SignatureMaker:
60
- def __get__(self, instance, cls) -> inspect.Signature: ...
65
+ def __get__(self, instance, cls=None) -> typing_extensions.Never: ...
61
66
 
62
67
  signature_maker: _SignatureMaker
63
68
 
@@ -141,7 +146,7 @@ class Field(metaclass=SlotMakerMeta):
141
146
 
142
147
  __slots__: dict[str, str]
143
148
  __classbuilder_internals__: dict
144
- __signature__: inspect.Signature
149
+ __signature__: _SignatureMaker
145
150
 
146
151
  def __init__(
147
152
  self,
@@ -1,2 +1,2 @@
1
- __version__ = "0.10.0"
2
- __version_tuple__ = (0, 10, 0)
1
+ __version__ = "0.10.2"
2
+ __version_tuple__ = (0, 10, 2)
@@ -24,15 +24,38 @@ import sys
24
24
 
25
25
  class _LazyAnnotationLib:
26
26
  def __getattr__(self, item):
27
- global _lazyannotationlib
27
+ global _lazy_annotationlib
28
28
  import annotationlib # type: ignore
29
- _lazyannotationlib = annotationlib
29
+ _lazy_annotationlib = annotationlib
30
30
  return getattr(annotationlib, item)
31
31
 
32
32
 
33
33
  _lazy_annotationlib = _LazyAnnotationLib()
34
34
 
35
35
 
36
+ def get_func_annotations(func):
37
+ """
38
+ Given a function, return the annotations dictionary
39
+
40
+ :param func: function object
41
+ :return: dictionary of annotations
42
+ """
43
+ # This method exists for use by prefab in getting annotations from
44
+ # the __prefab_post_init__ function
45
+ try:
46
+ annotations = func.__annotations__
47
+ except Exception:
48
+ if sys.version_info >= (3, 14):
49
+ annotations = _lazy_annotationlib.get_annotations(
50
+ func,
51
+ format=_lazy_annotationlib.Format.FORWARDREF,
52
+ )
53
+ else:
54
+ raise
55
+
56
+ return annotations
57
+
58
+
36
59
  def get_ns_annotations(ns):
37
60
  """
38
61
  Given a class namespace, attempt to retrieve the
@@ -60,6 +83,36 @@ def get_ns_annotations(ns):
60
83
  return annotations
61
84
 
62
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
+
63
116
  def is_classvar(hint):
64
117
  if isinstance(hint, str):
65
118
  # String annotations, just check if the string 'ClassVar' is in there
@@ -1,12 +1,21 @@
1
+ from collections.abc import Callable
1
2
  import typing
2
3
  import types
3
4
 
4
5
  _CopiableMappings = dict[str, typing.Any] | types.MappingProxyType[str, typing.Any]
5
6
 
7
+ def get_func_annotations(
8
+ func: types.FunctionType,
9
+ ) -> dict[str, typing.Any]: ...
10
+
6
11
  def get_ns_annotations(
7
12
  ns: _CopiableMappings,
8
13
  ) -> dict[str, typing.Any]: ...
9
14
 
15
+ def make_annotate_func(
16
+ annos: dict[str, typing.Any]
17
+ ) -> Callable[[int], dict[str, typing.Any]]: ...
18
+
10
19
  def is_classvar(
11
20
  hint: object,
12
21
  ) -> bool: ...
@@ -34,6 +34,8 @@ from . import (
34
34
  get_repr_generator,
35
35
  )
36
36
 
37
+ from .annotations import get_func_annotations
38
+
37
39
  # These aren't used but are re-exported for ease of use
38
40
  # noinspection PyUnresolvedReferences
39
41
  from . import SlotFields, KW_ONLY # noqa: F401
@@ -62,6 +64,7 @@ def get_attributes(cls):
62
64
  # Method Generators
63
65
  def init_generator(cls, funcname="__init__"):
64
66
  globs = {}
67
+ annotations = {}
65
68
  # Get the internals dictionary and prepare attributes
66
69
  attributes = get_attributes(cls)
67
70
  flags = get_flags(cls)
@@ -98,47 +101,35 @@ def init_generator(cls, funcname="__init__"):
98
101
  func_arglist.extend(arglist)
99
102
 
100
103
  if extra_funcname == POST_INIT_FUNC:
101
- post_init_annotations.update(func.__annotations__)
104
+ post_init_annotations.update(get_func_annotations(func))
102
105
 
103
106
  pos_arglist = []
104
107
  kw_only_arglist = []
105
108
  for name, attrib in attributes.items():
106
109
  # post_init annotations can be used to broaden types.
107
- if name in post_init_annotations:
108
- globs[f"_{name}_type"] = post_init_annotations[name]
109
- elif attrib.type is not NOTHING:
110
- globs[f"_{name}_type"] = attrib.type
111
-
112
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
+
113
116
  if attrib.default is not NOTHING:
114
117
  if isinstance(attrib.default, (str, int, float, bool)):
115
118
  # Just use the literal in these cases
116
- if attrib.type is NOTHING:
117
- arg = f"{name}={attrib.default!r}"
118
- else:
119
- arg = f"{name}: _{name}_type = {attrib.default!r}"
119
+ arg = f"{name}={attrib.default!r}"
120
120
  else:
121
121
  # No guarantee repr will work for other objects
122
122
  # so store the value in a variable and put it
123
123
  # in the globals dict for eval
124
- if attrib.type is NOTHING:
125
- arg = f"{name}=_{name}_default"
126
- else:
127
- arg = f"{name}: _{name}_type = _{name}_default"
124
+ arg = f"{name}=_{name}_default"
128
125
  globs[f"_{name}_default"] = attrib.default
129
126
  elif attrib.default_factory is not NOTHING:
130
127
  # Use NONE here and call the factory later
131
128
  # This matches the behaviour of compiled
132
- if attrib.type is NOTHING:
133
- arg = f"{name}=None"
134
- else:
135
- arg = f"{name}: _{name}_type = None"
129
+ arg = f"{name}=None"
136
130
  globs[f"_{name}_factory"] = attrib.default_factory
137
131
  else:
138
- if attrib.type is NOTHING:
139
- arg = name
140
- else:
141
- arg = f"{name}: _{name}_type"
132
+ arg = name
142
133
  if attrib.kw_only or kw_only:
143
134
  kw_only_arglist.append(arg)
144
135
  else:
@@ -208,9 +199,15 @@ def init_generator(cls, funcname="__init__"):
208
199
  f"{pre_init_call}\n"
209
200
  f"{body}\n"
210
201
  f"{post_init_call}\n"
211
- )
202
+ )
212
203
 
213
- 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)
214
211
 
215
212
 
216
213
  def iter_generator(cls, funcname="__iter__"):
@@ -13,6 +13,7 @@ from . import (
13
13
  GeneratedCode,
14
14
  MethodMaker,
15
15
  SlotMakerMeta,
16
+ _SignatureMaker
16
17
  )
17
18
 
18
19
  from . import SlotFields as SlotFields, KW_ONLY as KW_ONLY
@@ -47,7 +48,7 @@ hash_maker: MethodMaker
47
48
 
48
49
  class Attribute(Field):
49
50
  __slots__: dict
50
- __signature__: inspect.Signature
51
+ __signature__: _SignatureMaker
51
52
  __classbuilder_gathered_fields__: tuple[dict[str, Field], dict[str, typing.Any]]
52
53
 
53
54
  iter: bool
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ducktools-classbuilder
3
- Version: 0.10.0
3
+ Version: 0.10.2
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  Project-URL: Homepage, https://github.com/davidcellis/ducktools-classbuilder
@@ -0,0 +1,13 @@
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,,
@@ -1,13 +0,0 @@
1
- ducktools/classbuilder/__init__.py,sha256=nZz_pQM6CN7yCT71oOclih4uls73a6SlW-HY2gxzYIs,37244
2
- ducktools/classbuilder/__init__.pyi,sha256=v9QDO8A0AdPlAPNOXZGYXu1FaAq3yelwWNucdfWudrs,8143
3
- ducktools/classbuilder/_version.py,sha256=MdjuHfU3W8hJQpD3kGrPvowqtRDz5JMifDCCjNbrskE,54
4
- ducktools/classbuilder/annotations.py,sha256=GgBvNthDSRvKKZ9R_qxEVtCV3_vGuEBfuqQJFcDAe1s,3005
5
- ducktools/classbuilder/annotations.pyi,sha256=c5vYtULdDgMYWtkzeYMsHIbmnEuT2Ru-nNZieWvYuQ4,247
6
- ducktools/classbuilder/prefab.py,sha256=2GldQTRvOcFzB68qXbymeHKiXoyHx7eOjGxeilFIcn0,24632
7
- ducktools/classbuilder/prefab.pyi,sha256=q5Zca5wAN80GE4uICY6v3EIB6LvIcjOEhzD1pVDK44U,6507
8
- ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
9
- ducktools_classbuilder-0.10.0.dist-info/licenses/LICENSE,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
10
- ducktools_classbuilder-0.10.0.dist-info/METADATA,sha256=zrp9Q-klxkuTdsGSGz87rEZ7nxM8FCEppJfiGwqRh-0,9231
11
- ducktools_classbuilder-0.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- ducktools_classbuilder-0.10.0.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
13
- ducktools_classbuilder-0.10.0.dist-info/RECORD,,