ducktools-classbuilder 0.7.0__tar.gz → 0.7.1__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 (91) hide show
  1. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/PKG-INFO +2 -1
  2. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/extension_examples.md +2 -2
  3. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/tutorial.md +10 -8
  4. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs_code/docs_ex3_iterable.py +1 -1
  5. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs_code/tutorial_code.py +4 -2
  6. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/pyproject.toml +1 -0
  7. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools/classbuilder/__init__.py +0 -7
  8. ducktools_classbuilder-0.7.1/src/ducktools/classbuilder/_version.py +2 -0
  9. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools/classbuilder/annotations.py +106 -23
  10. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools/classbuilder/annotations.pyi +6 -0
  11. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools_classbuilder.egg-info/PKG-INFO +2 -1
  12. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools_classbuilder.egg-info/SOURCES.txt +3 -1
  13. ducktools_classbuilder-0.7.1/tests/annotations/test_future_annotations.py +45 -0
  14. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/conftest.py +2 -2
  15. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/dynamic/test_slotted_class.py +0 -1
  16. ducktools_classbuilder-0.7.1/tests/py314_tests/test_forwardref_annotations.py +45 -0
  17. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/test_core.py +0 -1
  18. ducktools_classbuilder-0.7.0/src/ducktools/classbuilder/_version.py +0 -2
  19. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/.github/dependabot.yml +0 -0
  20. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/.github/workflows/auto_test.yml +0 -0
  21. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/.github/workflows/publish_to_pypi.yml +0 -0
  22. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/.github/workflows/publish_to_testpypi.yml +0 -0
  23. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/.gitignore +0 -0
  24. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/.readthedocs.yaml +0 -0
  25. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/LICENSE.md +0 -0
  26. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/MANIFEST.in +0 -0
  27. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/README.md +0 -0
  28. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/Makefile +0 -0
  29. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/api.md +0 -0
  30. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/approach_vs_tool.md +0 -0
  31. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/conf.py +0 -0
  32. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/generated_code.md +0 -0
  33. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/index.md +0 -0
  34. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/make.bat +0 -0
  35. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/perf/performance_tests.md +0 -0
  36. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs/prefab/index.md +0 -0
  37. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs_code/docs_ex1_basic.py +0 -0
  38. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs_code/docs_ex2_register.py +0 -0
  39. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs_code/docs_ex5_frozen.py +0 -0
  40. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs_code/docs_ex7_posonly.py +0 -0
  41. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs_code/docs_ex8_converters.py +0 -0
  42. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/docs_code/docs_ex9_annotated.py +0 -0
  43. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/perf/cluegen.py +0 -0
  44. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/perf/dataklasses.py +0 -0
  45. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/perf/hyperfine_testmaker.py +0 -0
  46. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/perf/perf_profile.py +0 -0
  47. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/setup.cfg +0 -0
  48. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools/classbuilder/__init__.pyi +0 -0
  49. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools/classbuilder/prefab.py +0 -0
  50. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools/classbuilder/prefab.pyi +0 -0
  51. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools/classbuilder/py.typed +0 -0
  52. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  53. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  54. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  55. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/annotations/test_annotated.py +0 -0
  56. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/annotations/test_annotations_module.py +0 -0
  57. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  58. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/dynamic/test_construction.py +0 -0
  59. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/dynamic/test_frozen.py +0 -0
  60. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/dynamic/test_internals.py +0 -0
  61. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  62. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/dynamic/test_private.py +0 -0
  63. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  64. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/dynamic/test_subclass_implementation.py +0 -0
  65. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/conftest.py +0 -0
  66. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/creation.py +0 -0
  67. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/creation_empty.py +0 -0
  68. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/dunders.py +0 -0
  69. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
  70. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  71. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  72. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  73. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  74. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  75. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  76. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  77. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/inheritance.py +0 -0
  78. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/init_ex.py +0 -0
  79. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/kw_only.py +0 -0
  80. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/examples/repr_func.py +0 -0
  81. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/test_creation.py +0 -0
  82. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/test_dunders.py +0 -0
  83. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/test_funcs.py +0 -0
  84. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/test_hint_syntax.py +0 -0
  85. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/test_inheritance.py +0 -0
  86. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/test_init.py +0 -0
  87. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/test_kw_only.py +0 -0
  88. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/prefab/shared/test_repr.py +0 -0
  89. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/py312_tests/test_generic_annotations.py +0 -0
  90. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/test_field_flags.py +0 -0
  91. {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.1}/tests/test_slotmakermeta.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -32,6 +32,7 @@ Classifier: Programming Language :: Python :: 3.9
32
32
  Classifier: Programming Language :: Python :: 3.10
33
33
  Classifier: Programming Language :: Python :: 3.11
34
34
  Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
35
36
  Classifier: Operating System :: OS Independent
36
37
  Classifier: License :: OSI Approved :: MIT License
37
38
  Requires-Python: >=3.8
@@ -84,7 +84,7 @@ def iter_generator(cls, funcname="__iter__"):
84
84
  field_yield = "\n".join(f" yield self.{f}" for f in field_names)
85
85
  if not field_yield:
86
86
  field_yield = " yield from ()"
87
- code = f"def {funcname}(self):\n" f"{field_yield}"
87
+ code = f"def {funcname}(self):\n{field_yield}"
88
88
  globs = {}
89
89
  return GeneratedCode(code, globs)
90
90
 
@@ -330,7 +330,7 @@ if __name__ == "__main__":
330
330
 
331
331
  This seems to be a feature people keep requesting for `dataclasses`.
332
332
 
333
- To implement this you simply need to create a new annotated_gatherer function.
333
+ To implement this you need to create a new annotated_gatherer function.
334
334
 
335
335
  > Note: Field classes will be frozen when running under pytest.
336
336
  > They should not be mutated by gatherers.
@@ -1,6 +1,6 @@
1
1
  # Tutorial: Making a class boilerplate generator #
2
2
 
3
- The core idea is that there are 3 parts to the process of generating
3
+ The core idea is that there are 4 parts to the process of generating
4
4
  the class boilerplate that need to be handled:
5
5
 
6
6
  1. Create a new subclass of `Field` if you need to add any extra attributes to fields
@@ -162,7 +162,7 @@ print(report_generator(CodegenDemo).source_code)
162
162
  Here we will make both a simple decorator based builder and then a subclass
163
163
  based builder that can create `__slots__`.
164
164
 
165
- ### Decorator builder ###
165
+ ### 4a: Decorator builder ###
166
166
  ```python
167
167
  def reportclass(cls):
168
168
  gatherer = fields_attribute_gatherer
@@ -177,21 +177,22 @@ def reportclass(cls):
177
177
  flags = {"slotted": slotted}
178
178
 
179
179
  return dtbuild.builder(cls, gatherer=gatherer, methods=methods, flags=flags)
180
+ ```
180
181
 
181
- # Step 4b: Define a base class builder
182
+ ### 4b: Base class Builder ###
183
+ ```python
182
184
  # Once slots have been made, slot_gatherer should be used.
183
185
  slot_gatherer = dtbuild.make_slot_gatherer(CustomField)
184
- ```
185
186
 
186
- ### Base class Builder ###
187
- ```python
187
+
188
188
  class ReportClass(metaclass=dtbuild.SlotMakerMeta):
189
189
  __slots__ = {}
190
190
  _meta_gatherer = fields_attribute_gatherer
191
191
 
192
192
  def __init_subclass__(cls):
193
- slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
194
- gatherer = slot_gatherer if slotted else fields_attribute_gatherer
193
+ # Check if the metaclass has generated slots
194
+ meta_slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
195
+ gatherer = slot_gatherer if meta_slotted else fields_attribute_gatherer
195
196
  methods = {
196
197
  dtbuild.eq_maker,
197
198
  dtbuild.repr_maker,
@@ -199,6 +200,7 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
199
200
  report_maker
200
201
  }
201
202
 
203
+ # The class may still have slots unrelated to code generation
202
204
  slotted = "__slots__" in vars(cls)
203
205
  flags = {"slotted": slotted}
204
206
 
@@ -13,7 +13,7 @@ def iter_generator(cls, funcname="__iter__"):
13
13
  field_yield = "\n".join(f" yield self.{f}" for f in field_names)
14
14
  if not field_yield:
15
15
  field_yield = " yield from ()"
16
- code = f"def {funcname}(self):\n" f"{field_yield}"
16
+ code = f"def {funcname}(self):\n{field_yield}"
17
17
  globs = {}
18
18
  return GeneratedCode(code, globs)
19
19
 
@@ -117,8 +117,9 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
117
117
  _meta_gatherer = fields_attribute_gatherer
118
118
 
119
119
  def __init_subclass__(cls):
120
- slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
121
- gatherer = slot_gatherer if slotted else fields_attribute_gatherer
120
+ # Check if the metaclass has generated slots
121
+ meta_slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
122
+ gatherer = slot_gatherer if meta_slotted else fields_attribute_gatherer
122
123
  methods = {
123
124
  dtbuild.eq_maker,
124
125
  dtbuild.repr_maker,
@@ -126,6 +127,7 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
126
127
  report_maker
127
128
  }
128
129
 
130
+ # The class may still have slots unrelated to code generation
129
131
  slotted = "__slots__" in vars(cls)
130
132
  flags = {"slotted": slotted}
131
133
 
@@ -20,6 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.10",
21
21
  "Programming Language :: Python :: 3.11",
22
22
  "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
23
24
  "Operating System :: OS Independent",
24
25
  "License :: OSI Approved :: MIT License",
25
26
  ]
@@ -707,10 +707,6 @@ def make_slot_gatherer(field_type=Field):
707
707
  "in order to generate a slotclass"
708
708
  )
709
709
 
710
- # Don't want to mutate original annotations so make a copy if it exists
711
- # Looking at the dict is a Python3.9 or earlier requirement
712
- cls_annotations = get_ns_annotations(cls_dict)
713
-
714
710
  cls_fields = {}
715
711
  slot_replacement = {}
716
712
 
@@ -724,8 +720,6 @@ def make_slot_gatherer(field_type=Field):
724
720
 
725
721
  if isinstance(v, field_type):
726
722
  attrib = v
727
- if attrib.type is not NOTHING:
728
- cls_annotations[k] = attrib.type
729
723
  else:
730
724
  # Plain values treated as defaults
731
725
  attrib = field_type(default=v)
@@ -738,7 +732,6 @@ def make_slot_gatherer(field_type=Field):
738
732
  # In this case, slots with documentation and new annotations.
739
733
  modifications = {
740
734
  "__slots__": slot_replacement,
741
- "__annotations__": cls_annotations,
742
735
  }
743
736
 
744
737
  return cls_fields, modifications
@@ -0,0 +1,2 @@
1
+ __version__ = "0.7.1"
2
+ __version_tuple__ = (0, 7, 1)
@@ -24,6 +24,19 @@ import sys
24
24
  import builtins
25
25
 
26
26
 
27
+ # Evil stuff from types.py
28
+ def _cell_factory():
29
+ a = 1
30
+
31
+ def f():
32
+ nonlocal a
33
+ return f.__closure__[0]
34
+ _FunctionType = type(_cell_factory)
35
+ _CellType = type(_cell_factory())
36
+ del _cell_factory
37
+ # End evil stuff from types.py
38
+
39
+
27
40
  class _StringGlobs(dict):
28
41
  """
29
42
  Based on the fake globals dictionary used for annotations
@@ -99,6 +112,62 @@ def eval_hint(hint, context=None, *, recursion_limit=2):
99
112
  return hint
100
113
 
101
114
 
115
+ def call_annotate_func(annotate):
116
+ # Python 3.14 breaks the old methods of getting annotations
117
+ # The new annotationlib currently relies on 'ast' and 'functools'
118
+ # that this project tries to avoid importing.
119
+
120
+ # The basic logic is copied from there, however, replacing ForwardRef
121
+ # with a more basic class.
122
+ # While `annotationlib` is trying to return ForwardRef objects that can
123
+ # be evaluated later, this module only cares about annotations that can
124
+ # be evaluated at the point this function is called.
125
+ # As such we throw away the other information and just return strings
126
+ # instead of forwardrefs.
127
+
128
+ try:
129
+ raw_annotations = annotate(1)
130
+ except NameError:
131
+ pass
132
+ else:
133
+ return raw_annotations
134
+
135
+ # The annotate func may support forwardref natively
136
+ try:
137
+ raw_annotations = annotate(2)
138
+ except NotImplementedError:
139
+ pass
140
+ else:
141
+ return raw_annotations
142
+
143
+ # Not supported so we have to implement our own deferred handling
144
+ # Some modified logic from annotationlib
145
+ namespace = {**annotate.__builtins__, **annotate.__globals__}
146
+ globs = _StringGlobs(namespace)
147
+
148
+ # This handles closures where the variable is defined after get annotations is called.
149
+ if annotate.__closure__:
150
+ freevars = annotate.__code__.co_freevars
151
+ new_closure = []
152
+ for i, cell in enumerate(annotate.__closure__):
153
+ try:
154
+ cell.cell_contents
155
+ except ValueError:
156
+ if i < len(freevars):
157
+ name = freevars[i]
158
+ else:
159
+ name = "__cell__"
160
+ new_closure.append(_CellType(name))
161
+ else:
162
+ new_closure.append(cell)
163
+ closure = tuple(new_closure)
164
+ else:
165
+ closure = None
166
+
167
+ new_annotate = _FunctionType(annotate.__code__, globs, closure=closure)
168
+ return new_annotate(1)
169
+
170
+
102
171
  def get_ns_annotations(ns, eval_str=True):
103
172
  """
104
173
  Given a class namespace, attempt to retrieve the
@@ -112,36 +181,50 @@ def get_ns_annotations(ns, eval_str=True):
112
181
  :param eval_str: Attempt to evaluate string annotations (default to True)
113
182
  :return: dictionary of evaluated annotations
114
183
  """
115
- raw_annotations = ns.get("__annotations__", {})
116
184
 
117
- if not eval_str:
118
- return raw_annotations.copy()
185
+ # In 3.14 the 'canonical' method of getting annotations is to use __annotate__
186
+ # If this doesn't exist, check __annotations__ and treat as 3.13 or earlier.
187
+ annotate = ns.get("__annotate__")
119
188
 
120
- try:
121
- obj_modulename = ns["__module__"]
122
- except KeyError:
123
- obj_module = None
189
+ if annotate is not None:
190
+ raw_annotations = call_annotate_func(annotate)
124
191
  else:
125
- obj_module = sys.modules.get(obj_modulename, None)
192
+ raw_annotations = ns.get("__annotations__", {})
126
193
 
127
- if obj_module:
128
- obj_globals = vars(obj_module)
129
- else:
130
- obj_globals = {}
194
+ # Unlike annotationlib we still try to evaluate string annotations
195
+ # This will catch cases where someone has used a literal string for a
196
+ # single attribute.
197
+ if eval_str:
198
+ try:
199
+ obj_modulename = ns["__module__"]
200
+ except KeyError:
201
+ obj_module = None
202
+ else:
203
+ obj_module = sys.modules.get(obj_modulename, None)
204
+
205
+ if obj_module:
206
+ obj_globals = vars(obj_module)
207
+ else:
208
+ obj_globals = {}
131
209
 
132
- # Type parameters should be usable in hints without breaking
133
- # This is for Python 3.12+
134
- type_params = {
135
- repr(param): param
136
- for param in ns.get("__type_params__", ())
137
- }
210
+ # Type parameters should be usable in hints without breaking
211
+ # This is for Python 3.12+
212
+ type_params = {
213
+ repr(param): param
214
+ for param in ns.get("__type_params__", ())
215
+ }
138
216
 
139
- context = {**vars(builtins), **obj_globals, **type_params, **ns}
217
+ context = {**vars(builtins), **obj_globals, **type_params, **ns}
218
+
219
+ annotations = {
220
+ k: eval_hint(v, context)
221
+ for k, v in raw_annotations.items()
222
+ }
223
+
224
+ else:
225
+ annotations = raw_annotations.copy()
140
226
 
141
- return {
142
- k: eval_hint(v, context)
143
- for k, v in raw_annotations.items()
144
- }
227
+ return annotations
145
228
 
146
229
 
147
230
  def is_classvar(hint):
@@ -1,3 +1,4 @@
1
+ from collections.abc import Callable
1
2
  import typing
2
3
  import types
3
4
 
@@ -8,6 +9,11 @@ class _StringGlobs(dict):
8
9
  def __missing__(self, key: _T) -> _T: ...
9
10
 
10
11
 
12
+ def call_annotate_func(
13
+ annotate: Callable[[int], dict[str, type | typing.ForwardRef]]
14
+ ) -> dict[str, type | str]: ...
15
+
16
+
11
17
  def eval_hint(
12
18
  hint: type | str,
13
19
  context: None | dict[str, typing.Any] = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -32,6 +32,7 @@ Classifier: Programming Language :: Python :: 3.9
32
32
  Classifier: Programming Language :: Python :: 3.10
33
33
  Classifier: Programming Language :: Python :: 3.11
34
34
  Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
35
36
  Classifier: Operating System :: OS Independent
36
37
  Classifier: License :: OSI Approved :: MIT License
37
38
  Requires-Python: >=3.8
@@ -50,6 +50,7 @@ tests/test_field_flags.py
50
50
  tests/test_slotmakermeta.py
51
51
  tests/annotations/test_annotated.py
52
52
  tests/annotations/test_annotations_module.py
53
+ tests/annotations/test_future_annotations.py
53
54
  tests/prefab/dynamic/test_compare_attrib.py
54
55
  tests/prefab/dynamic/test_construction.py
55
56
  tests/prefab/dynamic/test_frozen.py
@@ -83,4 +84,5 @@ tests/prefab/shared/examples/fails/creation_3.py
83
84
  tests/prefab/shared/examples/fails/creation_5.py
84
85
  tests/prefab/shared/examples/fails/inheritance_1.py
85
86
  tests/prefab/shared/examples/fails/inheritance_2.py
86
- tests/py312_tests/test_generic_annotations.py
87
+ tests/py312_tests/test_generic_annotations.py
88
+ tests/py314_tests/test_forwardref_annotations.py
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from ducktools.classbuilder.annotations import get_ns_annotations
4
+
5
+ from pathlib import Path
6
+
7
+ global_type = int
8
+
9
+ def test_bare_forwardref():
10
+ class Ex:
11
+ a: str
12
+ b: Path
13
+ c: plain_forwardref
14
+
15
+ annos = get_ns_annotations(Ex.__dict__)
16
+
17
+ assert annos == {'a': str, 'b': Path, 'c': "plain_forwardref"}
18
+
19
+
20
+ def test_inner_outer_ref():
21
+
22
+ def make_func():
23
+ inner_type = str
24
+
25
+ class Inner:
26
+ a_val: inner_type = "hello"
27
+ b_val: global_type = 42
28
+ c_val: hyper_type = 3.14
29
+
30
+ # Try to get annotations before hyper_type exists
31
+ annos = get_ns_annotations(Inner.__dict__)
32
+
33
+ hyper_type = float
34
+
35
+ return Inner, annos
36
+
37
+ cls, annos = make_func()
38
+
39
+ # Only global types can be evaluated
40
+ assert annos == {"a_val": "inner_type", "b_val": int, "c_val": "hyper_type"}
41
+
42
+ # No extra evaluation
43
+ assert get_ns_annotations(cls.__dict__) == {
44
+ "a_val": "inner_type", "b_val": int, "c_val": "hyper_type"
45
+ }
@@ -2,9 +2,9 @@ import sys
2
2
 
3
3
  collect_ignore = []
4
4
 
5
- if sys.version_info < (3, 14):
5
+ if sys.version_info < (3, 15):
6
6
  minor_ver = sys.version_info.minor
7
7
 
8
8
  collect_ignore.extend(
9
- f"py3{i+1}_tests" for i in range(minor_ver, 14)
9
+ f"py3{i+1}_tests" for i in range(minor_ver, 15)
10
10
  )
@@ -13,7 +13,6 @@ def test_basic_slotted():
13
13
  )
14
14
 
15
15
  assert SlottedPrefab.__slots__ == {"x": None, "y": "Digits of pi"}
16
- assert get_ns_annotations(SlottedPrefab.__dict__) == {"y": float}
17
16
 
18
17
  ex = SlottedPrefab()
19
18
 
@@ -0,0 +1,45 @@
1
+ # Bare forwardrefs only work in 3.14 or later
2
+
3
+ from ducktools.classbuilder.annotations import get_ns_annotations
4
+
5
+ from pathlib import Path
6
+
7
+ global_type = int
8
+
9
+ def test_bare_forwardref():
10
+ class Ex:
11
+ a: str
12
+ b: Path
13
+ c: plain_forwardref
14
+
15
+ annos = get_ns_annotations(Ex.__dict__)
16
+
17
+ assert annos == {'a': str, 'b': Path, 'c': "plain_forwardref"}
18
+
19
+
20
+ def test_inner_outer_ref():
21
+
22
+ def make_func():
23
+ inner_type = str
24
+
25
+ class Inner:
26
+ a_val: inner_type = "hello"
27
+ b_val: global_type = 42
28
+ c_val: hyper_type = 3.14
29
+
30
+ # Try to get annotations before hyper_type exists
31
+ annos = get_ns_annotations(Inner.__dict__)
32
+
33
+ hyper_type = float
34
+
35
+ return Inner, annos
36
+
37
+ cls, annos = make_func()
38
+
39
+ # Forwardref given as string if used before it can be evaluated
40
+ assert annos == {"a_val": str, "b_val": int, "c_val": "hyper_type"}
41
+
42
+ # Correctly evaluated if it exists
43
+ assert get_ns_annotations(cls.__dict__) == {
44
+ "a_val": str, "b_val": int, "c_val": float
45
+ }
@@ -188,7 +188,6 @@ def test_slot_gatherer_success():
188
188
 
189
189
  assert slots == fields
190
190
  assert modifications["__slots__"] == {"a": None, "b": None, "c": "a list", "d": None}
191
- assert modifications["__annotations__"] == {"a": int, "d": str}
192
191
  assert get_ns_annotations(SlotsExample.__dict__) == {"a": int} # Original annotations dict unmodified
193
192
 
194
193
 
@@ -1,2 +0,0 @@
1
- __version__ = "0.7.0"
2
- __version_tuple__ = (0, 7, 0)