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.
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/PKG-INFO +1 -1
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/pyproject.toml +3 -2
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/__init__.py +52 -23
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/__init__.pyi +11 -6
- ducktools_classbuilder-0.10.2/src/ducktools/classbuilder/_version.py +2 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/annotations.py +55 -2
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/annotations.pyi +9 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/prefab.py +21 -24
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/prefab.pyi +2 -1
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/PKG-INFO +1 -1
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/SOURCES.txt +3 -1
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_init.py +28 -2
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/py314_tests/test_forwardref_annotations.py +12 -1
- ducktools_classbuilder-0.10.2/tests/py314_tests/test_init_signature.py +122 -0
- ducktools_classbuilder-0.10.2/tests/py314_tests/test_init_signature_future.py +105 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/uv.lock +133 -104
- ducktools_classbuilder-0.10.0/src/ducktools/classbuilder/_version.py +0 -2
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.github/dependabot.yml +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.github/workflows/auto_test.yml +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.github/workflows/publish_to_pypi.yml +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.github/workflows/publish_to_testpypi.yml +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.gitignore +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/.readthedocs.yaml +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/LICENSE +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/MANIFEST.in +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/README.md +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/Makefile +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/api.md +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/approach_vs_tool.md +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/conf.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/extension_examples.md +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/generated_code.md +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/index.md +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/make.bat +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/perf/performance_tests.md +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/prefab/index.md +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs/tutorial.md +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex10_frozen_attributes.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex1_basic.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex2_register.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex3_iterable.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex5_frozen.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex7_posonly.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex8_converters.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/docs_ex9_annotated.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/index_example.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/docs_code/tutorial_code.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/setup.cfg +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/py.typed +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/annotations/test_annotated.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/annotations/test_annotations_module.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/annotations/test_future_annotations.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/conftest.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/helpers/utils.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_compare_attrib.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_construction.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_creation.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_dunders.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_frozen.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_funcs.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_hint_syntax.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_inheritance.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_internals_dict.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_kw_only.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_pre_post_init.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_private.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_replace.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_repr.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_slots_novalues.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_slotted_class.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/prefab/test_subclass_implementation.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/py312_tests/test_generic_annotations.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/py314_tests/_test_support.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/test_core.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/test_field_flags.py +0 -0
- {ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/tests/test_slotmakermeta.py +0 -0
|
@@ -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
|
|
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) == (
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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,
|
|
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__(
|
|
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) ->
|
|
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__:
|
|
149
|
+
__signature__: _SignatureMaker
|
|
145
150
|
|
|
146
151
|
def __init__(
|
|
147
152
|
self,
|
|
@@ -24,15 +24,38 @@ import sys
|
|
|
24
24
|
|
|
25
25
|
class _LazyAnnotationLib:
|
|
26
26
|
def __getattr__(self, item):
|
|
27
|
-
global
|
|
27
|
+
global _lazy_annotationlib
|
|
28
28
|
import annotationlib # type: ignore
|
|
29
|
-
|
|
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: ...
|
{ducktools_classbuilder-0.10.0 → ducktools_classbuilder-0.10.2}/src/ducktools/classbuilder/prefab.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__:
|
|
51
|
+
__signature__: _SignatureMaker
|
|
51
52
|
__classbuilder_gathered_fields__: tuple[dict[str, Field], dict[str, typing.Any]]
|
|
52
53
|
|
|
53
54
|
iter: bool
|
|
@@ -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)
|