ducktools-classbuilder 0.7.5__tar.gz → 0.8.0__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 (95) hide show
  1. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/PKG-INFO +15 -15
  2. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/README.md +14 -14
  3. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/api.md +0 -4
  4. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/extension_examples.md +2 -1
  5. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/generated_code.md +16 -10
  6. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/index.md +35 -6
  7. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/tutorial.md +8 -2
  8. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex9_annotated.py +0 -2
  9. ducktools_classbuilder-0.8.0/docs_code/index_example.py +34 -0
  10. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/tutorial_code.py +7 -4
  11. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/__init__.py +2 -2
  12. ducktools_classbuilder-0.8.0/src/ducktools/classbuilder/_version.py +2 -0
  13. ducktools_classbuilder-0.8.0/src/ducktools/classbuilder/annotations.py +91 -0
  14. ducktools_classbuilder-0.8.0/src/ducktools/classbuilder/annotations.pyi +12 -0
  15. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/PKG-INFO +15 -15
  16. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/SOURCES.txt +1 -0
  17. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/annotations/test_annotated.py +1 -1
  18. ducktools_classbuilder-0.8.0/tests/annotations/test_annotations_module.py +58 -0
  19. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/annotations/test_future_annotations.py +17 -3
  20. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/py312_tests/test_generic_annotations.py +1 -1
  21. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/test_slotmakermeta.py +1 -1
  22. ducktools_classbuilder-0.7.5/src/ducktools/classbuilder/_version.py +0 -2
  23. ducktools_classbuilder-0.7.5/src/ducktools/classbuilder/annotations.py +0 -223
  24. ducktools_classbuilder-0.7.5/src/ducktools/classbuilder/annotations.pyi +0 -27
  25. ducktools_classbuilder-0.7.5/tests/annotations/test_annotations_module.py +0 -118
  26. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.github/dependabot.yml +0 -0
  27. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.github/workflows/auto_test.yml +0 -0
  28. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.github/workflows/publish_to_pypi.yml +0 -0
  29. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.github/workflows/publish_to_testpypi.yml +0 -0
  30. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.gitignore +0 -0
  31. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.readthedocs.yaml +0 -0
  32. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/LICENSE.md +0 -0
  33. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/MANIFEST.in +0 -0
  34. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/Makefile +0 -0
  35. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/approach_vs_tool.md +0 -0
  36. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/conf.py +0 -0
  37. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/make.bat +0 -0
  38. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/perf/performance_tests.md +0 -0
  39. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/prefab/index.md +0 -0
  40. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex10_frozen_attributes.py +0 -0
  41. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex1_basic.py +0 -0
  42. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex2_register.py +0 -0
  43. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex3_iterable.py +0 -0
  44. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex5_frozen.py +0 -0
  45. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex7_posonly.py +0 -0
  46. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex8_converters.py +0 -0
  47. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/perf/cluegen.py +0 -0
  48. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/perf/dataklasses.py +0 -0
  49. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/perf/hyperfine_testmaker.py +0 -0
  50. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/perf/perf_profile.py +0 -0
  51. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/pyproject.toml +0 -0
  52. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/setup.cfg +0 -0
  53. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/__init__.pyi +0 -0
  54. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/prefab.py +0 -0
  55. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/prefab.pyi +0 -0
  56. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/py.typed +0 -0
  57. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  58. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
  59. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  60. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/conftest.py +0 -0
  61. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  62. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_construction.py +0 -0
  63. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_frozen.py +0 -0
  64. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_internals.py +0 -0
  65. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  66. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_private.py +0 -0
  67. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
  68. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_slotted_class.py +0 -0
  69. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_subclass_implementation.py +0 -0
  70. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/conftest.py +0 -0
  71. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/creation.py +0 -0
  72. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/creation_empty.py +0 -0
  73. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/dunders.py +0 -0
  74. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  75. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  76. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  77. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  78. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  79. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  80. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  81. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/inheritance.py +0 -0
  82. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/init_ex.py +0 -0
  83. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/kw_only.py +0 -0
  84. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/repr_func.py +0 -0
  85. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_creation.py +0 -0
  86. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_dunders.py +0 -0
  87. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_funcs.py +0 -0
  88. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_hint_syntax.py +0 -0
  89. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_inheritance.py +0 -0
  90. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_init.py +0 -0
  91. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_kw_only.py +0 -0
  92. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_repr.py +0 -0
  93. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/py314_tests/test_forwardref_annotations.py +0 -0
  94. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/test_core.py +0 -0
  95. {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/test_field_flags.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.7.5
3
+ Version: 0.8.0
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -86,10 +86,12 @@ These tools are available from the main `ducktools.classbuilder` module.
86
86
  * `@slotclass`
87
87
  * A decorator based implementation that uses a special dict subclass assigned
88
88
  to `__slots__` to describe the fields for method generation.
89
- * `AnnotationClass`
90
- * A subclass based implementation that works with `__slots__`, type annotations
91
- or `Field(...)` attributes to describe the fields for method generation.
92
- * If `__slots__` isn't used to declare fields, it will be generated by a metaclass.
89
+ * `SlotMakerMeta`
90
+ * A metaclass for creating other implementations using annotations, fields or slots.
91
+ * This metaclass will allow for creating `__slots__` correctly in subclasses.
92
+ * `builder`
93
+ * This is the main tool used for constructing decorators and base classes to provide
94
+ generated methods.
93
95
 
94
96
  Each of these forms of class generation will result in the same methods being
95
97
  attached to the class after the field information has been obtained.
@@ -119,8 +121,9 @@ This includes more customization including `__prefab_pre_init__` and `__prefab_p
119
121
  functions for subclass customization.
120
122
 
121
123
  A `@prefab` decorator and `Prefab` base class are provided.
122
- Similar to `AnnotationClass`, `Prefab` will generate `__slots__` by default.
123
- However decorated classes with `@prefab` that do not declare fields using `__slots__`
124
+
125
+ `Prefab` will generate `__slots__` by default.
126
+ decorated classes with `@prefab` that do not declare fields using `__slots__`
124
127
  will **not** be slotted and there is no `slots` argument to apply this.
125
128
 
126
129
  Here is an example of applying a conversion in `__post_init__`:
@@ -164,7 +167,7 @@ the fields can be set *before* the class is constructed, so the class
164
167
  will work correctly without needing to be rebuilt.
165
168
 
166
169
  For example these two classes would be roughly equivalent, except that
167
- `@dataclass` has had to recreate the class from scratch while `AnnotationClass`
170
+ `@dataclass` has had to recreate the class from scratch while `Prefab`
168
171
  has created `__slots__` and added the methods on to the original class.
169
172
  This means that any references stored to the original class *before*
170
173
  `@dataclass` has rebuilt the class will not be pointing towards the
@@ -179,7 +182,7 @@ functions.
179
182
  ```python
180
183
  import json
181
184
  from dataclasses import dataclass
182
- from ducktools.classbuilder import AnnotationClass, Field
185
+ from ducktools.classbuilder.prefab import Prefab, attribute
183
186
 
184
187
 
185
188
  class _RegisterDescriptor:
@@ -222,10 +225,10 @@ class DataCoords:
222
225
  return {"x": self.x, "y": self.y}
223
226
 
224
227
 
225
- # slots=True is the default for AnnotationClass
226
- class BuilderCoords(AnnotationClass, slots=True):
228
+ # slots=True is the default for Prefab
229
+ class BuilderCoords(Prefab):
227
230
  x: float = 0.0
228
- y: float = Field(default=0.0, doc="y coordinate")
231
+ y: float = attribute(default=0.0, doc="y coordinate")
229
232
 
230
233
  @register.register_method
231
234
  def to_json(self):
@@ -289,9 +292,6 @@ It will copy values provided as the `type` to `Field` into the
289
292
  Values provided to `doc` will be placed in the final `__slots__`
290
293
  field so they are present on the class if `help(...)` is called.
291
294
 
292
- `AnnotationClass` offers the same features with additional methods of gathering
293
- fields.
294
-
295
295
  If you want something with more features you can look at the `prefab`
296
296
  submodule which provides more specific features that differ further from the
297
297
  behaviour of `dataclasses`.
@@ -32,10 +32,12 @@ These tools are available from the main `ducktools.classbuilder` module.
32
32
  * `@slotclass`
33
33
  * A decorator based implementation that uses a special dict subclass assigned
34
34
  to `__slots__` to describe the fields for method generation.
35
- * `AnnotationClass`
36
- * A subclass based implementation that works with `__slots__`, type annotations
37
- or `Field(...)` attributes to describe the fields for method generation.
38
- * If `__slots__` isn't used to declare fields, it will be generated by a metaclass.
35
+ * `SlotMakerMeta`
36
+ * A metaclass for creating other implementations using annotations, fields or slots.
37
+ * This metaclass will allow for creating `__slots__` correctly in subclasses.
38
+ * `builder`
39
+ * This is the main tool used for constructing decorators and base classes to provide
40
+ generated methods.
39
41
 
40
42
  Each of these forms of class generation will result in the same methods being
41
43
  attached to the class after the field information has been obtained.
@@ -65,8 +67,9 @@ This includes more customization including `__prefab_pre_init__` and `__prefab_p
65
67
  functions for subclass customization.
66
68
 
67
69
  A `@prefab` decorator and `Prefab` base class are provided.
68
- Similar to `AnnotationClass`, `Prefab` will generate `__slots__` by default.
69
- However decorated classes with `@prefab` that do not declare fields using `__slots__`
70
+
71
+ `Prefab` will generate `__slots__` by default.
72
+ decorated classes with `@prefab` that do not declare fields using `__slots__`
70
73
  will **not** be slotted and there is no `slots` argument to apply this.
71
74
 
72
75
  Here is an example of applying a conversion in `__post_init__`:
@@ -110,7 +113,7 @@ the fields can be set *before* the class is constructed, so the class
110
113
  will work correctly without needing to be rebuilt.
111
114
 
112
115
  For example these two classes would be roughly equivalent, except that
113
- `@dataclass` has had to recreate the class from scratch while `AnnotationClass`
116
+ `@dataclass` has had to recreate the class from scratch while `Prefab`
114
117
  has created `__slots__` and added the methods on to the original class.
115
118
  This means that any references stored to the original class *before*
116
119
  `@dataclass` has rebuilt the class will not be pointing towards the
@@ -125,7 +128,7 @@ functions.
125
128
  ```python
126
129
  import json
127
130
  from dataclasses import dataclass
128
- from ducktools.classbuilder import AnnotationClass, Field
131
+ from ducktools.classbuilder.prefab import Prefab, attribute
129
132
 
130
133
 
131
134
  class _RegisterDescriptor:
@@ -168,10 +171,10 @@ class DataCoords:
168
171
  return {"x": self.x, "y": self.y}
169
172
 
170
173
 
171
- # slots=True is the default for AnnotationClass
172
- class BuilderCoords(AnnotationClass, slots=True):
174
+ # slots=True is the default for Prefab
175
+ class BuilderCoords(Prefab):
173
176
  x: float = 0.0
174
- y: float = Field(default=0.0, doc="y coordinate")
177
+ y: float = attribute(default=0.0, doc="y coordinate")
175
178
 
176
179
  @register.register_method
177
180
  def to_json(self):
@@ -235,9 +238,6 @@ It will copy values provided as the `type` to `Field` into the
235
238
  Values provided to `doc` will be placed in the final `__slots__`
236
239
  field so they are present on the class if `help(...)` is called.
237
240
 
238
- `AnnotationClass` offers the same features with additional methods of gathering
239
- fields.
240
-
241
241
  If you want something with more features you can look at the `prefab`
242
242
  submodule which provides more specific features that differ further from the
243
243
  behaviour of `dataclasses`.
@@ -40,7 +40,3 @@
40
40
  ```{eval-rst}
41
41
  .. autofunction:: ducktools.classbuilder.annotations::make_annotation_gatherer
42
42
  ```
43
-
44
- ```{eval-rst}
45
- .. autoclass:: ducktools.classbuilder.annotations::AnnotationClass
46
- ```
@@ -469,7 +469,8 @@ To implement this you need to create a new annotated_gatherer function.
469
469
  > If you need to change the value of a field use Field.from_field(...) to make a new instance.
470
470
 
471
471
  ```python
472
- from __future__ import annotations
472
+ # Don't use __future__ annotations with get_ns_annotations in this case
473
+ # as it doesn't evaluate string annotations.
473
474
 
474
475
  import types
475
476
  from typing import Annotated, Any, ClassVar, get_origin
@@ -7,11 +7,14 @@ There is a helper function `get_methods` that can be used to obtain the names an
7
7
  methods that have been attached to the class by the builder.
8
8
 
9
9
  ```python
10
- from ducktools.classbuilder import AnnotationClass, get_methods
10
+ from ducktools.classbuilder import SlotFields, get_methods, slotclass
11
11
 
12
- class Example(AnnotationClass):
13
- a: str = "a"
14
- b: str = "b"
12
+ @slotclass
13
+ class Example:
14
+ __slots__ = SlotFields(
15
+ a='a',
16
+ b='b'
17
+ )
15
18
 
16
19
  print(get_methods(Example))
17
20
  ```
@@ -28,12 +31,15 @@ These can then be used to examine the generated source code and global variables
28
31
  to the `exec` function. This can be useful for debugging code generators.
29
32
 
30
33
  ```python
31
- from ducktools.classbuilder import AnnotationClass, get_methods
32
-
33
- class Example(AnnotationClass):
34
- a: str = "a"
35
- b: str = "b"
36
-
34
+ from ducktools.classbuilder import SlotFields, get_methods, slotclass
35
+
36
+ @slotclass
37
+ class Example:
38
+ __slots__ = SlotFields(
39
+ a='a',
40
+ b='b',
41
+ )
42
+
37
43
  methods = get_methods(Example)
38
44
 
39
45
  for method in methods.values():
@@ -65,15 +65,44 @@ ex = SlottedDC()
65
65
  print(ex)
66
66
  ```
67
67
 
68
- ## Annotation Class Usage ##
68
+ ## Using Annotations ##
69
69
 
70
- There is an additional AnnotationClass base class that allows creating slotted classes
71
- using annotations. This has to be a base class with a specific metaclass in order to
72
- create the `__slots__` field *before* the class has been generated in order to work
73
- correctly.
70
+ It is possible to create slotted classes using Annotations.
71
+ There is a `Prefab` base class in the `prefab` submodule that does this,
72
+ but it also easy to implement using the provided tools.
73
+
74
+ In order to correctly implement `__slots__` this needs to be done
75
+ using a metaclass as `__slots__` must be defined before the **class**
76
+ is created.
74
77
 
75
78
  ```python
76
- from ducktools.classbuilder import AnnotationClass
79
+ from ducktools.classbuilder import (
80
+ SlotMakerMeta,
81
+ annotation_gatherer,
82
+ builder,
83
+ check_argument_order,
84
+ default_methods,
85
+ )
86
+
87
+
88
+ class AnnotationClass(metaclass=SlotMakerMeta):
89
+ __slots__ = {}
90
+
91
+ def __init_subclass__(
92
+ cls,
93
+ methods=default_methods,
94
+ gatherer=annotation_gatherer,
95
+ **kwargs
96
+ ):
97
+ # Check class dict otherwise this will always be True as this base
98
+ # class uses slots.
99
+ slots = "__slots__" in cls.__dict__
100
+
101
+ builder(cls, gatherer=gatherer, methods=methods, flags={"slotted": slots})
102
+ check_argument_order(cls)
103
+ super().__init_subclass__(**kwargs)
104
+
105
+
77
106
 
78
107
  class AnnotatedDC(AnnotationClass):
79
108
  the_answer: int = 42
@@ -146,9 +146,15 @@ def report_generator(cls, funcname="report"):
146
146
  report_maker = dtbuild.MethodMaker("report", report_generator)
147
147
  ```
148
148
 
149
- We can take a quick look at what this generates by applying it to an `AnnotationClass`:
149
+ We can take a quick look at what this generates by applying it to a `slotclass`:
150
150
  ```python
151
- class CodegenDemo(dtbuild.AnnotationClass):
151
+ @dtbuild.slotclass
152
+ class CodegenDemo:
153
+ __slots__ = dtbuild.SlotFields(
154
+ field_1="Field one",
155
+ field_2="Field two",
156
+ field_3="Field three",
157
+ )
152
158
  field_1: str = "Field one"
153
159
  field_2: str = "Field two"
154
160
  field_3: str = "Field three"
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import types
4
2
  from typing import Annotated, Any, ClassVar, get_origin
5
3
 
@@ -0,0 +1,34 @@
1
+ from ducktools.classbuilder import (
2
+ SlotMakerMeta,
3
+ builder,
4
+ check_argument_order,
5
+ default_methods,
6
+ unified_gatherer,
7
+ )
8
+
9
+
10
+ class AnnotationClass(metaclass=SlotMakerMeta):
11
+ __slots__ = {}
12
+
13
+ def __init_subclass__(
14
+ cls,
15
+ methods=default_methods,
16
+ gatherer=unified_gatherer,
17
+ **kwargs
18
+ ):
19
+ # Check class dict otherwise this will always be True as this base
20
+ # class uses slots.
21
+ slots = "__slots__" in cls.__dict__
22
+
23
+ builder(cls, gatherer=gatherer, methods=methods, flags={"slotted": slots})
24
+ check_argument_order(cls)
25
+ super().__init_subclass__(**kwargs)
26
+
27
+
28
+ class AnnotatedDC(AnnotationClass):
29
+ the_answer: int = 42
30
+ the_question: str = "What do you get if you multiply six by nine?"
31
+
32
+
33
+ ex = AnnotatedDC()
34
+ print(ex)
@@ -81,10 +81,13 @@ report_maker = dtbuild.MethodMaker("report", report_generator)
81
81
 
82
82
 
83
83
  # View the generated code by testing on a demo class
84
- class CodegenDemo(dtbuild.AnnotationClass):
85
- field_1: str = "Field one"
86
- field_2: str = "Field two"
87
- field_3: str = "Field three"
84
+ @dtbuild.slotclass
85
+ class CodegenDemo:
86
+ __slots__ = dtbuild.SlotFields(
87
+ field_1="Field one",
88
+ field_2="Field two",
89
+ field_3="Field three",
90
+ )
88
91
 
89
92
 
90
93
  print(report_generator(CodegenDemo).source_code)
@@ -769,7 +769,7 @@ def make_annotation_gatherer(
769
769
  if is_classvar(v):
770
770
  continue
771
771
 
772
- if v is KW_ONLY:
772
+ if v is KW_ONLY or (isinstance(v, str) and v == "KW_ONLY"):
773
773
  if kw_flag:
774
774
  raise SyntaxError("KW_ONLY sentinel may only appear once.")
775
775
  kw_flag = True
@@ -868,7 +868,7 @@ def make_unified_gatherer(
868
868
  # To choose between annotation and attribute gatherers
869
869
  # compare sets of names.
870
870
  # Don't bother evaluating string annotations, as we only need names
871
- cls_annotations = get_ns_annotations(cls_dict, eval_str=False)
871
+ cls_annotations = get_ns_annotations(cls_dict)
872
872
  cls_attributes = {
873
873
  k: v for k, v in cls_dict.items() if isinstance(v, field_type)
874
874
  }
@@ -0,0 +1,2 @@
1
+ __version__ = "0.8.0"
2
+ __version_tuple__ = (0, 8, 0)
@@ -0,0 +1,91 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2024 David C Ellis
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+ import sys
23
+
24
+
25
+ def get_ns_annotations(ns):
26
+ """
27
+ Given a class namespace, attempt to retrieve the
28
+ annotations dictionary.
29
+
30
+ :param ns: Class namespace (eg cls.__dict__)
31
+ :return: dictionary of annotations
32
+ """
33
+
34
+ annotations = ns.get("__annotations__")
35
+ if annotations is not None:
36
+ annotations = annotations.copy()
37
+ else:
38
+ # See if we're using PEP-649 annotations
39
+ # Guarding this with a try/except instead of a version check
40
+ # In case there's a change and PEP-649 somehow doesn't make 3.14
41
+ try:
42
+ from annotationlib import Format, call_annotate_function, get_annotate_function
43
+ except ImportError:
44
+ pass
45
+ else:
46
+ annotate = ns.get("__annotate__") # Works in the alphas, but may break
47
+ if not annotate:
48
+ annotate = get_annotate_function(ns)
49
+ if annotate:
50
+ annotations = call_annotate_function(annotate, format=Format.FORWARDREF)
51
+
52
+ if annotations is None:
53
+ annotations = {}
54
+
55
+ return annotations
56
+
57
+
58
+ def is_classvar(hint):
59
+ if isinstance(hint, str):
60
+ # String annotations, just check if the string 'ClassVar' is in there
61
+ # This is overly broad and could be smarter.
62
+ return "ClassVar" in hint
63
+ elif (annotationlib := sys.modules.get("annotationlib")) and isinstance(hint, annotationlib.ForwardRef):
64
+ return "ClassVar" in hint.__arg__
65
+ else:
66
+ _typing = sys.modules.get("typing")
67
+ if _typing:
68
+ # Annotated is a nightmare I'm never waking up from
69
+ # 3.8 and 3.9 need Annotated from typing_extensions
70
+ # 3.8 also needs get_origin from typing_extensions
71
+ if sys.version_info < (3, 10):
72
+ _typing_extensions = sys.modules.get("typing_extensions")
73
+ if _typing_extensions:
74
+ _Annotated = _typing_extensions.Annotated
75
+ _get_origin = _typing_extensions.get_origin
76
+ else:
77
+ _Annotated, _get_origin = None, None
78
+ else:
79
+ _Annotated = _typing.Annotated
80
+ _get_origin = _typing.get_origin
81
+
82
+ if _Annotated and _get_origin(hint) is _Annotated:
83
+ hint = getattr(hint, "__origin__", None)
84
+
85
+ if (
86
+ hint is _typing.ClassVar
87
+ or getattr(hint, "__origin__", None) is _typing.ClassVar
88
+ ):
89
+ return True
90
+ return False
91
+
@@ -0,0 +1,12 @@
1
+ import typing
2
+ import types
3
+
4
+ _CopiableMappings = dict[str, typing.Any] | types.MappingProxyType[str, typing.Any]
5
+
6
+ def get_ns_annotations(
7
+ ns: _CopiableMappings,
8
+ ) -> dict[str, typing.Any]: ...
9
+
10
+ def is_classvar(
11
+ hint: object,
12
+ ) -> bool: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ducktools-classbuilder
3
- Version: 0.7.5
3
+ Version: 0.8.0
4
4
  Summary: Toolkit for creating class boilerplate generators
5
5
  Author: David C Ellis
6
6
  License: MIT License
@@ -86,10 +86,12 @@ These tools are available from the main `ducktools.classbuilder` module.
86
86
  * `@slotclass`
87
87
  * A decorator based implementation that uses a special dict subclass assigned
88
88
  to `__slots__` to describe the fields for method generation.
89
- * `AnnotationClass`
90
- * A subclass based implementation that works with `__slots__`, type annotations
91
- or `Field(...)` attributes to describe the fields for method generation.
92
- * If `__slots__` isn't used to declare fields, it will be generated by a metaclass.
89
+ * `SlotMakerMeta`
90
+ * A metaclass for creating other implementations using annotations, fields or slots.
91
+ * This metaclass will allow for creating `__slots__` correctly in subclasses.
92
+ * `builder`
93
+ * This is the main tool used for constructing decorators and base classes to provide
94
+ generated methods.
93
95
 
94
96
  Each of these forms of class generation will result in the same methods being
95
97
  attached to the class after the field information has been obtained.
@@ -119,8 +121,9 @@ This includes more customization including `__prefab_pre_init__` and `__prefab_p
119
121
  functions for subclass customization.
120
122
 
121
123
  A `@prefab` decorator and `Prefab` base class are provided.
122
- Similar to `AnnotationClass`, `Prefab` will generate `__slots__` by default.
123
- However decorated classes with `@prefab` that do not declare fields using `__slots__`
124
+
125
+ `Prefab` will generate `__slots__` by default.
126
+ decorated classes with `@prefab` that do not declare fields using `__slots__`
124
127
  will **not** be slotted and there is no `slots` argument to apply this.
125
128
 
126
129
  Here is an example of applying a conversion in `__post_init__`:
@@ -164,7 +167,7 @@ the fields can be set *before* the class is constructed, so the class
164
167
  will work correctly without needing to be rebuilt.
165
168
 
166
169
  For example these two classes would be roughly equivalent, except that
167
- `@dataclass` has had to recreate the class from scratch while `AnnotationClass`
170
+ `@dataclass` has had to recreate the class from scratch while `Prefab`
168
171
  has created `__slots__` and added the methods on to the original class.
169
172
  This means that any references stored to the original class *before*
170
173
  `@dataclass` has rebuilt the class will not be pointing towards the
@@ -179,7 +182,7 @@ functions.
179
182
  ```python
180
183
  import json
181
184
  from dataclasses import dataclass
182
- from ducktools.classbuilder import AnnotationClass, Field
185
+ from ducktools.classbuilder.prefab import Prefab, attribute
183
186
 
184
187
 
185
188
  class _RegisterDescriptor:
@@ -222,10 +225,10 @@ class DataCoords:
222
225
  return {"x": self.x, "y": self.y}
223
226
 
224
227
 
225
- # slots=True is the default for AnnotationClass
226
- class BuilderCoords(AnnotationClass, slots=True):
228
+ # slots=True is the default for Prefab
229
+ class BuilderCoords(Prefab):
227
230
  x: float = 0.0
228
- y: float = Field(default=0.0, doc="y coordinate")
231
+ y: float = attribute(default=0.0, doc="y coordinate")
229
232
 
230
233
  @register.register_method
231
234
  def to_json(self):
@@ -289,9 +292,6 @@ It will copy values provided as the `type` to `Field` into the
289
292
  Values provided to `doc` will be placed in the final `__slots__`
290
293
  field so they are present on the class if `help(...)` is called.
291
294
 
292
- `AnnotationClass` offers the same features with additional methods of gathering
293
- fields.
294
-
295
295
  If you want something with more features you can look at the `prefab`
296
296
  submodule which provides more specific features that differ further from the
297
297
  behaviour of `dataclasses`.
@@ -27,6 +27,7 @@ docs_code/docs_ex5_frozen.py
27
27
  docs_code/docs_ex7_posonly.py
28
28
  docs_code/docs_ex8_converters.py
29
29
  docs_code/docs_ex9_annotated.py
30
+ docs_code/index_example.py
30
31
  docs_code/tutorial_code.py
31
32
  perf/cluegen.py
32
33
  perf/dataklasses.py
@@ -139,6 +139,6 @@ def test_annotationclass():
139
139
  def test_annotated_syntax_error():
140
140
  with pytest.raises(SyntaxError):
141
141
  class ExampleAnnotated(AnnotationClass):
142
- a: str = "a"
142
+ a: str = "a" # noqa: the error being highlighted is the error we are testing.
143
143
  b: "list[str]"
144
144
  c: Annotated[str, ""] = Field(default="c")
@@ -0,0 +1,58 @@
1
+ import builtins
2
+
3
+ from ducktools.classbuilder.annotations import (
4
+ get_ns_annotations,
5
+ is_classvar,
6
+ )
7
+ from typing import List, ClassVar
8
+ from typing_extensions import Annotated
9
+
10
+
11
+ def test_ns_annotations():
12
+ CV = ClassVar
13
+
14
+ class AnnotatedClass:
15
+ a: str
16
+ b: "str"
17
+ c: List[str]
18
+ d: "List[str]"
19
+ e: ClassVar[str]
20
+ f: "ClassVar[str]"
21
+ g: "ClassVar[forwardref]"
22
+ h: "Annotated[ClassVar[str], '']"
23
+ i: "Annotated[ClassVar[forwardref], '']"
24
+ j: "CV[str]"
25
+
26
+ annos = get_ns_annotations(vars(AnnotatedClass))
27
+
28
+ assert annos == {
29
+ 'a': str,
30
+ 'b': "str",
31
+ 'c': List[str],
32
+ 'd': "List[str]",
33
+ 'e': ClassVar[str],
34
+ 'f': "ClassVar[str]",
35
+ 'g': "ClassVar[forwardref]",
36
+ 'h': "Annotated[ClassVar[str], '']",
37
+ 'i': "Annotated[ClassVar[forwardref], '']",
38
+ 'j': "CV[str]",
39
+ }
40
+
41
+
42
+ def test_is_classvar():
43
+ assert is_classvar(ClassVar)
44
+ assert is_classvar(ClassVar[str])
45
+ assert is_classvar(ClassVar['forwardref'])
46
+
47
+ assert is_classvar(Annotated[ClassVar[str], ''])
48
+ assert is_classvar(Annotated[ClassVar['forwardref'], ''])
49
+
50
+ assert is_classvar("ClassVar")
51
+ assert is_classvar("ClassVar[str]")
52
+ assert is_classvar("ClassVar['forwardref']")
53
+
54
+ assert is_classvar("Annotated[ClassVar[str], '']")
55
+ assert is_classvar("Annotated[ClassVar['forwardref'], '']")
56
+
57
+ assert not is_classvar(str)
58
+ assert not is_classvar(Annotated[str, ''])
@@ -14,7 +14,7 @@ def test_bare_forwardref():
14
14
 
15
15
  annos = get_ns_annotations(Ex.__dict__)
16
16
 
17
- assert annos == {'a': str, 'b': Path, 'c': "plain_forwardref"}
17
+ assert annos == {'a': "str", 'b': "Path", 'c': "plain_forwardref"}
18
18
 
19
19
 
20
20
  def test_inner_outer_ref():
@@ -37,9 +37,23 @@ def test_inner_outer_ref():
37
37
  cls, annos = make_func()
38
38
 
39
39
  # Only global types can be evaluated
40
- assert annos == {"a_val": "inner_type", "b_val": int, "c_val": "hyper_type"}
40
+ assert annos == {"a_val": "inner_type", "b_val": "global_type", "c_val": "hyper_type"}
41
41
 
42
42
  # No extra evaluation
43
43
  assert get_ns_annotations(cls.__dict__) == {
44
- "a_val": "inner_type", "b_val": int, "c_val": "hyper_type"
44
+ "a_val": "inner_type", "b_val": "global_type", "c_val": "hyper_type"
45
45
  }
46
+
47
+
48
+ def test_not_evaluated():
49
+ class EvalCheck:
50
+ def __class_getitem__(cls, item):
51
+ raise KeyError("This should not be raised")
52
+ def __getattr__(self, key):
53
+ raise AttributeError("This should also not be raised")
54
+
55
+ class DontEval:
56
+ a: EvalCheck['str'] # The test is that the exception does not occur as this is not evaluated
57
+ b: EvalCheck.missing_attribute
58
+
59
+ get_ns_annotations(DontEval.__dict__)