ducktools-classbuilder 0.10.0__tar.gz → 0.10.2__tar.gz

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.

Files changed (79) hide show
  1. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/PKG-INFO +1 -1
  2. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/pyproject.toml +3 -2
  3. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/__init__.py +52 -23
  4. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/__init__.pyi +11 -6
  5. ducktools_classbuilder-0.10.2/src/ducktools/classbuilder/_version.py +2 -0
  6. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/annotations.py +55 -2
  7. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/annotations.pyi +9 -0
  8. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/prefab.py +21 -24
  9. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/prefab.pyi +2 -1
  10. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/PKG-INFO +1 -1
  11. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/SOURCES.txt +3 -1
  12. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_init.py +28 -2
  13. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/py314_tests/test_forwardref_annotations.py +12 -1
  14. ducktools_classbuilder-0.10.2/tests/py314_tests/test_init_signature.py +122 -0
  15. ducktools_classbuilder-0.10.2/tests/py314_tests/test_init_signature_future.py +105 -0
  16. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/uv.lock +133 -104
  17. ducktools_classbuilder-0.10.0/src/ducktools/classbuilder/_version.py +0 -2
  18. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.github/dependabot.yml +0 -0
  19. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.github/workflows/auto_test.yml +0 -0
  20. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.github/workflows/publish_to_pypi.yml +0 -0
  21. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.github/workflows/publish_to_testpypi.yml +0 -0
  22. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.gitignore +0 -0
  23. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.readthedocs.yaml +0 -0
  24. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/LICENSE +0 -0
  25. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/MANIFEST.in +0 -0
  26. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/README.md +0 -0
  27. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/Makefile +0 -0
  28. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/api.md +0 -0
  29. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/approach_vs_tool.md +0 -0
  30. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/conf.py +0 -0
  31. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/extension_examples.md +0 -0
  32. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/generated_code.md +0 -0
  33. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/index.md +0 -0
  34. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/make.bat +0 -0
  35. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/perf/performance_tests.md +0 -0
  36. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/prefab/index.md +0 -0
  37. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/tutorial.md +0 -0
  38. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex10_frozen_attributes.py +0 -0
  39. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex1_basic.py +0 -0
  40. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex2_register.py +0 -0
  41. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex3_iterable.py +0 -0
  42. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex5_frozen.py +0 -0
  43. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex7_posonly.py +0 -0
  44. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex8_converters.py +0 -0
  45. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex9_annotated.py +0 -0
  46. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/index_example.py +0 -0
  47. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/tutorial_code.py +0 -0
  48. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/setup.cfg +0 -0
  49. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/py.typed +0 -0
  50. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  51. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  52. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  53. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/annotations/test_annotated.py +0 -0
  54. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/annotations/test_annotations_module.py +0 -0
  55. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/annotations/test_future_annotations.py +0 -0
  56. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/conftest.py +0 -0
  57. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/helpers/utils.py +0 -0
  58. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_compare_attrib.py +0 -0
  59. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_construction.py +0 -0
  60. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_creation.py +0 -0
  61. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_dunders.py +0 -0
  62. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_frozen.py +0 -0
  63. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_funcs.py +0 -0
  64. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_hint_syntax.py +0 -0
  65. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_inheritance.py +0 -0
  66. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_internals_dict.py +0 -0
  67. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_kw_only.py +0 -0
  68. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_pre_post_init.py +0 -0
  69. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_private.py +0 -0
  70. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_replace.py +0 -0
  71. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_repr.py +0 -0
  72. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_slots_novalues.py +0 -0
  73. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_slotted_class.py +0 -0
  74. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_subclass_implementation.py +0 -0
  75. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/py312_tests/test_generic_annotations.py +0 -0
  76. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/py314_tests/_test_support.py +0 -0
  77. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/test_core.py +0 -0
  78. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/test_field_flags.py +0 -0
  79. {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/test_slotmakermeta.py +0 -0
@@ -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
@@ -28,8 +28,8 @@ dynamic = ['version']
28
28
  [project.optional-dependencies]
29
29
  # Needed for the current readthedocs.yaml
30
30
  docs = [
31
- "sphinx>=8.1",
32
- "myst-parser>=4.0",
31
+ "sphinx>=8.1",
32
+ "myst-parser>=4.0",
33
33
  "sphinx_rtd_theme>=3.0",
34
34
  ]
35
35
 
@@ -38,6 +38,7 @@ dev = [
38
38
  "pytest>=8.4",
39
39
  "pytest-cov>=6.1",
40
40
  "mypy>=1.16",
41
+ "typing-extensions>=4.14",
41
42
  ]
42
43
  performance = [
43
44
  "attrs>=25.0",
@@ -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,
@@ -0,0 +1,2 @@
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
@@ -71,4 +71,6 @@ tests/prefab/test_slotted_class.py
71
71
  tests/prefab/test_subclass_implementation.py
72
72
  tests/py312_tests/test_generic_annotations.py
73
73
  tests/py314_tests/_test_support.py
74
- tests/py314_tests/test_forwardref_annotations.py
74
+ tests/py314_tests/test_forwardref_annotations.py
75
+ tests/py314_tests/test_init_signature.py
76
+ tests/py314_tests/test_init_signature_future.py
@@ -260,11 +260,37 @@ def test_signature():
260
260
  import inspect
261
261
 
262
262
  init_sig = inspect.signature(TypeSignatureInit.__init__)
263
- assert str(init_sig) == "(self, x: int, y: str = 'Test')"
263
+ assert str(init_sig) == "(self, x: int, y: str = 'Test') -> None"
264
264
 
265
265
 
266
266
  def test_partial_signature():
267
267
  import inspect
268
268
 
269
269
  init_sig = inspect.signature(PartialTypeSignatureInit.__init__)
270
- assert str(init_sig) == "(self, x, y: str = 'Test')"
270
+ assert str(init_sig) == "(self, x, y: str = 'Test') -> None"
271
+
272
+
273
+ def test_inherited_signature():
274
+ import inspect
275
+
276
+ @prefab
277
+ class Base:
278
+ x: int
279
+ y: str = "Base"
280
+
281
+ class Inherited(Base):
282
+ def __init__(self, x=42, y="Inherited") -> None:
283
+ self.x = x
284
+ self.y = y
285
+
286
+ base_signature = inspect.signature(Base)
287
+ inherited_signature = inspect.signature(Inherited)
288
+
289
+ assert str(base_signature) == "(x: int, y: str = 'Base') -> None"
290
+ assert str(inherited_signature) == "(x=42, y='Inherited') -> None"
291
+
292
+ with pytest.raises(AttributeError):
293
+ Base.__signature__
294
+
295
+ with pytest.raises(AttributeError):
296
+ Inherited.__signature__
@@ -1,6 +1,6 @@
1
1
  # Bare forwardrefs only work in 3.14 or later
2
2
 
3
- from ducktools.classbuilder.annotations import get_ns_annotations
3
+ from ducktools.classbuilder.annotations import get_ns_annotations, get_func_annotations
4
4
 
5
5
  from pathlib import Path
6
6
 
@@ -46,3 +46,14 @@ def test_inner_outer_ref():
46
46
  assert get_ns_annotations(cls.__dict__) == {
47
47
  "a_val": str, "b_val": int, "c_val": float
48
48
  }
49
+
50
+
51
+ def test_func_annotations():
52
+ def forwardref_func(x: unknown) -> str:
53
+ return ''
54
+
55
+ annos = get_func_annotations(forwardref_func)
56
+ assert annos == {
57
+ 'x': EqualToForwardRef("unknown", owner=forwardref_func),
58
+ 'return': str
59
+ }
@@ -0,0 +1,122 @@
1
+ from annotationlib import get_annotations, Format
2
+
3
+ import pytest
4
+
5
+
6
+ from ducktools.classbuilder.prefab import Prefab, prefab
7
+ from _test_support import EqualToForwardRef
8
+
9
+
10
+ # Aliases for alias test
11
+ assign_int = int
12
+ type type_str = str
13
+
14
+
15
+ @pytest.mark.parametrize(
16
+ ["format", "expected"],
17
+ [
18
+ (Format.VALUE, {"return": None, "x": str, "y": int}),
19
+ (Format.FORWARDREF, {"return": None, "x": str, "y": int}),
20
+ (Format.STRING, {"return": "None", "x": "str", "y": "int"}),
21
+ ]
22
+ )
23
+ def test_resolvable_annotations(format, expected):
24
+ @prefab
25
+ class Example:
26
+ x: str
27
+ y: int
28
+
29
+ annos = get_annotations(Example.__init__, format=format)
30
+
31
+ assert annos == expected
32
+
33
+ class Example(Prefab):
34
+ x: str
35
+ y: int
36
+
37
+ annos = get_annotations(Example.__init__, format=format)
38
+
39
+ assert annos == expected
40
+
41
+
42
+ @pytest.mark.parametrize(
43
+ ["format", "expected"],
44
+ [
45
+ (Format.VALUE, {"return": None, "x": str, "y": int}),
46
+ (Format.FORWARDREF, {"return": None, "x": str, "y": int}),
47
+ (Format.STRING, {"return": "None", "x": "str", "y": "late_definition"}),
48
+ ]
49
+ )
50
+ def test_late_defined_annotations(format, expected):
51
+ # Test where the annotation is a forwardref at processing time
52
+ @prefab
53
+ class Example:
54
+ x: str
55
+ y: late_definition
56
+
57
+ late_definition = int
58
+
59
+ annos = get_annotations(Example.__init__, format=format)
60
+
61
+ assert annos == expected
62
+
63
+
64
+ @pytest.mark.parametrize(
65
+ ["format", "expected"],
66
+ [
67
+ (Format.VALUE, {"return": None, "x": int, "y": type_str}),
68
+ (Format.FORWARDREF, {"return": None, "x": int, "y": type_str}),
69
+ (Format.STRING, {"return": "None", "x": "int", "y": "type_str"}),
70
+ ]
71
+ )
72
+ def test_alias_defined_annotations(format, expected):
73
+ # Test the behaviour of type aliases and regular types
74
+ # Type Alias names should be kept while regular assignments will be lost
75
+
76
+ @prefab
77
+ class Example:
78
+ x: assign_int # type: ignore
79
+ y: type_str
80
+
81
+ annos = get_annotations(Example.__init__, format=format)
82
+
83
+ assert annos == expected
84
+
85
+
86
+ @pytest.mark.parametrize(
87
+ ["format", "expected"],
88
+ [
89
+ (Format.FORWARDREF, {"return": None, "x": str, "y": EqualToForwardRef("undefined")}),
90
+ (Format.STRING, {"return": "None", "x": "str", "y": "undefined"}),
91
+ ]
92
+ )
93
+ def test_forwardref_annotation(format, expected):
94
+ # Test where the annotation is a forwardref at processing and analysis
95
+ class Example(Prefab):
96
+ x: str
97
+ y: undefined
98
+
99
+ annos = get_annotations(Example.__init__, format=format)
100
+
101
+ assert annos == expected
102
+
103
+
104
+ def test_forwardref_raises():
105
+ # Should still raise a NameError with VALUE annotations
106
+ @prefab
107
+ class Example:
108
+ x: str
109
+ y: undefined
110
+
111
+ with pytest.raises(NameError):
112
+ annos = get_annotations(Example.__init__, format=Format.VALUE)
113
+
114
+
115
+ def test_raises_with_fake_globals():
116
+ @prefab
117
+ class Example:
118
+ x: str
119
+ y: undefined
120
+
121
+ with pytest.raises(NotImplementedError):
122
+ annos = Example.__init__.__annotate__(Format.VALUE_WITH_FAKE_GLOBALS)