ducktools-classbuilder 0.5.0__tar.gz → 0.6.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 (76) hide show
  1. ducktools_classbuilder-0.6.0/PKG-INFO +318 -0
  2. ducktools_classbuilder-0.6.0/README.md +266 -0
  3. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/docs/api.md +2 -6
  4. ducktools_classbuilder-0.6.0/docs/extension_examples.md +528 -0
  5. ducktools_classbuilder-0.6.0/docs/generated_code.md +41 -0
  6. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/docs/index.md +9 -9
  7. ducktools_classbuilder-0.6.0/docs/perf/performance_tests.md +76 -0
  8. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/docs/prefab/index.md +5 -6
  9. ducktools_classbuilder-0.6.0/docs/tutorial.md +234 -0
  10. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/pyproject.toml +2 -1
  11. ducktools_classbuilder-0.6.0/src/ducktools/classbuilder/__init__.py +945 -0
  12. ducktools_classbuilder-0.6.0/src/ducktools/classbuilder/__init__.pyi +258 -0
  13. ducktools_classbuilder-0.6.0/src/ducktools/classbuilder/annotations.py +173 -0
  14. ducktools_classbuilder-0.6.0/src/ducktools/classbuilder/annotations.pyi +26 -0
  15. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/src/ducktools/classbuilder/prefab.py +120 -230
  16. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/src/ducktools/classbuilder/prefab.pyi +28 -22
  17. ducktools_classbuilder-0.6.0/src/ducktools_classbuilder.egg-info/PKG-INFO +318 -0
  18. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/src/ducktools_classbuilder.egg-info/SOURCES.txt +12 -2
  19. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/src/ducktools_classbuilder.egg-info/requires.txt +4 -2
  20. {ducktools_classbuilder-0.5.0/tests → ducktools_classbuilder-0.6.0/tests/annotations}/test_annotated.py +14 -11
  21. ducktools_classbuilder-0.6.0/tests/annotations/test_annotations_module.py +118 -0
  22. ducktools_classbuilder-0.6.0/tests/conftest.py +12 -0
  23. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/dynamic/test_construction.py +23 -0
  24. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/dynamic/test_slots_novalues.py +1 -1
  25. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/dynamic/test_slotted_class.py +2 -1
  26. ducktools_classbuilder-0.6.0/tests/prefab/dynamic/test_subclass_implementation.py +180 -0
  27. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/test_creation.py +12 -9
  28. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/test_dunders.py +1 -1
  29. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/test_init.py +2 -3
  30. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/test_kw_only.py +4 -2
  31. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/test_repr.py +4 -4
  32. ducktools_classbuilder-0.6.0/tests/py312_tests/test_generic_annotations.py +15 -0
  33. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/test_core.py +174 -21
  34. ducktools_classbuilder-0.6.0/tests/test_field_flags.py +101 -0
  35. ducktools_classbuilder-0.6.0/tests/test_slotmakermeta.py +69 -0
  36. ducktools_classbuilder-0.5.0/PKG-INFO +0 -270
  37. ducktools_classbuilder-0.5.0/README.md +0 -221
  38. ducktools_classbuilder-0.5.0/docs/extension_examples.md +0 -916
  39. ducktools_classbuilder-0.5.0/docs/perf/performance_tests.md +0 -66
  40. ducktools_classbuilder-0.5.0/src/ducktools/classbuilder/__init__.py +0 -631
  41. ducktools_classbuilder-0.5.0/src/ducktools/classbuilder/__init__.pyi +0 -154
  42. ducktools_classbuilder-0.5.0/src/ducktools_classbuilder.egg-info/PKG-INFO +0 -270
  43. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/LICENSE.md +0 -0
  44. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/MANIFEST.in +0 -0
  45. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/docs/Makefile +0 -0
  46. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/docs/approach_vs_tool.md +0 -0
  47. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/docs/conf.py +0 -0
  48. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/docs/make.bat +0 -0
  49. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/setup.cfg +0 -0
  50. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/src/ducktools/classbuilder/py.typed +0 -0
  51. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
  52. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
  53. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
  54. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/dynamic/test_internals.py +0 -0
  55. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
  56. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/conftest.py +0 -0
  57. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/creation.py +0 -0
  58. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/creation_empty.py +0 -0
  59. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/dunders.py +0 -0
  60. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
  61. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
  62. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
  63. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
  64. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
  65. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
  66. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/frozen_prefabs.py +0 -0
  67. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
  68. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/hint_syntax.py +0 -0
  69. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/inheritance.py +0 -0
  70. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/init_ex.py +0 -0
  71. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/kw_only.py +0 -0
  72. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/examples/repr_func.py +0 -0
  73. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/test_frozen.py +0 -0
  74. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/test_funcs.py +0 -0
  75. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/test_hint_syntax.py +0 -0
  76. {ducktools_classbuilder-0.5.0 → ducktools_classbuilder-0.6.0}/tests/prefab/shared/test_inheritance.py +0 -0
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.1
2
+ Name: ducktools-classbuilder
3
+ Version: 0.6.0
4
+ Summary: Toolkit for creating class boilerplate generators
5
+ Author: David C Ellis
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 David C Ellis
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/davidcellis/ducktools-classbuilder
29
+ Classifier: Development Status :: 4 - Beta
30
+ Classifier: Programming Language :: Python :: 3.8
31
+ Classifier: Programming Language :: Python :: 3.9
32
+ Classifier: Programming Language :: Python :: 3.10
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Requires-Python: >=3.8
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE.md
40
+ Provides-Extra: testing
41
+ Requires-Dist: pytest; extra == "testing"
42
+ Requires-Dist: pytest-cov; extra == "testing"
43
+ Requires-Dist: mypy; extra == "testing"
44
+ Requires-Dist: typing_extensions; extra == "testing"
45
+ Provides-Extra: performance-tests
46
+ Requires-Dist: attrs; extra == "performance-tests"
47
+ Requires-Dist: pydantic; extra == "performance-tests"
48
+ Provides-Extra: docs
49
+ Requires-Dist: sphinx; extra == "docs"
50
+ Requires-Dist: myst-parser; extra == "docs"
51
+ Requires-Dist: sphinx_rtd_theme; extra == "docs"
52
+
53
+ # Ducktools: Class Builder #
54
+
55
+ `ducktools-classbuilder` is *the* Python package that will bring you the **joy**
56
+ of writing... functions... that will bring back the **joy** of writing classes.
57
+
58
+ Maybe.
59
+
60
+ While `attrs` and `dataclasses` are class boilerplate generators,
61
+ `ducktools.classbuilder` is intended to provide the tools to help make a customized
62
+ version of the same concept.
63
+
64
+ Install from PyPI with:
65
+ `python -m pip install ducktools-classbuilder`
66
+
67
+ ## Included Implementations ##
68
+
69
+ There are 2 different implementations provided with the module each of which offers
70
+ a subclass based and decorator based option.
71
+
72
+ > [!TIP]
73
+ > For more information on using these tools to create your own implementations
74
+ > using the builder see
75
+ > [the tutorial](https://ducktools-classbuilder.readthedocs.io/en/latest/tutorial.html)
76
+ > for a full tutorial and
77
+ > [extension_examples](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
78
+ > for other customizations.
79
+
80
+ ### Core ###
81
+
82
+ These tools are available from the main `ducktools.classbuilder` module.
83
+
84
+ * `@slotclass`
85
+ * A decorator based implementation that uses a special dict subclass assigned
86
+ to `__slots__` to describe the fields for method generation.
87
+ * `AnnotationClass`
88
+ * A subclass based implementation that works with `__slots__`, type annotations
89
+ or `Field(...)` attributes to describe the fields for method generation.
90
+ * If `__slots__` isn't used to declare fields, it will be generated by a metaclass.
91
+
92
+ Each of these forms of class generation will result in the same methods being
93
+ attached to the class after the field information has been obtained.
94
+
95
+ ```python
96
+ from ducktools.classbuilder import Field, SlotFields, slotclass
97
+
98
+ @slotclass
99
+ class SlottedDC:
100
+ __slots__ = SlotFields(
101
+ the_answer=42,
102
+ the_question=Field(
103
+ default="What do you get if you multiply six by nine?",
104
+ doc="Life, the Universe, and Everything",
105
+ ),
106
+ )
107
+
108
+ ex = SlottedDC()
109
+ print(ex)
110
+ ```
111
+
112
+ ### Prefab ###
113
+
114
+ This is available from the `ducktools.classbuilder.prefab` submodule.
115
+
116
+ This includes more customization including `__prefab_pre_init__` and `__prefab_post_init__`
117
+ functions for subclass customization.
118
+
119
+ A `@prefab` decorator and `Prefab` base class are provided.
120
+ Similar to `AnnotationClass`, `Prefab` will generate `__slots__` by default.
121
+ However decorated classes with `@prefab` that do not declare fields using `__slots__`
122
+ will **not** be slotted and there is no `slots` argument to apply this.
123
+
124
+ Here is an example of applying a conversion in `__post_init__`:
125
+ ```python
126
+ from pathlib import Path
127
+ from ducktools.classbuilder.prefab import Prefab
128
+
129
+ class AppDetails(Prefab, frozen=True):
130
+ app_name: str
131
+ app_path: Path
132
+
133
+ def __prefab_post_init__(self, app_path: str | Path):
134
+ # frozen in `Prefab` is implemented as a 'set-once' __setattr__ function.
135
+ # So we do not need to use `object.__setattr__` here
136
+ self.app_path = Path(app_path)
137
+
138
+ steam = AppDetails(
139
+ "Steam",
140
+ r"C:\Program Files (x86)\Steam\steam.exe"
141
+ )
142
+
143
+ print(steam)
144
+ ```
145
+
146
+
147
+ ## What is the issue with generating `__slots__` with a decorator ##
148
+
149
+ If you want to use `__slots__` in order to save memory you have to declare
150
+ them when the class is originally created as you can't add them later.
151
+
152
+ When you use `@dataclass(slots=True)`[^2] with `dataclasses`, the function
153
+ has to make a new class and attempt to copy over everything from the original.
154
+
155
+ This is because decorators operate on classes *after they have been created*
156
+ while slots need to be declared beforehand.
157
+ While you can change the value of `__slots__` after a class has been created,
158
+ this will have no effect on the internal structure of the class.
159
+
160
+ By using a metaclass or by declaring fields using `__slots__` however,
161
+ the fields can be set *before* the class is constructed, so the class
162
+ will work correctly without needing to be rebuilt.
163
+
164
+ For example these two classes would be roughly equivalent, except that
165
+ `@dataclass` has had to recreate the class from scratch while `AnnotationClass`
166
+ has created `__slots__` and added the methods on to the original class.
167
+ This means that any references stored to the original class *before*
168
+ `@dataclass` has rebuilt the class will not be pointing towards the
169
+ correct class.
170
+
171
+ Here's a demonstration of the issue using a registry for serialization
172
+ functions.
173
+
174
+ > This example requires Python 3.10 or later as earlier versions of
175
+ > `dataclasses` did not support the `slots` argument.
176
+
177
+ ```python
178
+ import json
179
+ from dataclasses import dataclass
180
+ from ducktools.classbuilder import AnnotationClass, Field
181
+
182
+
183
+ class _RegisterDescriptor:
184
+ def __init__(self, func, registry):
185
+ self.func = func
186
+ self.registry = registry
187
+
188
+ def __set_name__(self, owner, name):
189
+ self.registry.register(owner, self.func)
190
+ setattr(owner, name, self.func)
191
+
192
+
193
+ class SerializeRegister:
194
+ def __init__(self):
195
+ self.serializers = {}
196
+
197
+ def register(self, cls, func):
198
+ self.serializers[cls] = func
199
+
200
+ def register_method(self, method):
201
+ return _RegisterDescriptor(method, self)
202
+
203
+ def default(self, o):
204
+ try:
205
+ return self.serializers[type(o)](o)
206
+ except KeyError:
207
+ raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
208
+
209
+
210
+ register = SerializeRegister()
211
+
212
+
213
+ @dataclass(slots=True)
214
+ class DataCoords:
215
+ x: float = 0.0
216
+ y: float = 0.0
217
+
218
+ @register.register_method
219
+ def to_json(self):
220
+ return {"x": self.x, "y": self.y}
221
+
222
+
223
+ # slots=True is the default for AnnotationClass
224
+ class BuilderCoords(AnnotationClass, slots=True):
225
+ x: float = 0.0
226
+ y: float = Field(default=0.0, doc="y coordinate")
227
+
228
+ @register.register_method
229
+ def to_json(self):
230
+ return {"x": self.x, "y": self.y}
231
+
232
+
233
+ # In both cases __slots__ have been defined
234
+ print(f"{DataCoords.__slots__ = }")
235
+ print(f"{BuilderCoords.__slots__ = }\n")
236
+
237
+ data_ex = DataCoords()
238
+ builder_ex = BuilderCoords()
239
+
240
+ objs = [data_ex, builder_ex]
241
+
242
+ print(data_ex)
243
+ print(builder_ex)
244
+ print()
245
+
246
+ # Demonstrate you can not set values not defined in slots
247
+ for obj in objs:
248
+ try:
249
+ obj.z = 1.0
250
+ except AttributeError as e:
251
+ print(e)
252
+ print()
253
+
254
+ print("Attempt to serialize:")
255
+ for obj in objs:
256
+ try:
257
+ print(f"{type(obj).__name__}: {json.dumps(obj, default=register.default)}")
258
+ except TypeError as e:
259
+ print(f"{type(obj).__name__}: {e!r}")
260
+ ```
261
+
262
+ Output (Python 3.12):
263
+ ```
264
+ DataCoords.__slots__ = ('x', 'y')
265
+ BuilderCoords.__slots__ = {'x': None, 'y': 'y coordinate'}
266
+
267
+ DataCoords(x=0.0, y=0.0)
268
+ BuilderCoords(x=0.0, y=0.0)
269
+
270
+ 'DataCoords' object has no attribute 'z'
271
+ 'BuilderCoords' object has no attribute 'z'
272
+
273
+ Attempt to serialize:
274
+ DataCoords: TypeError('Object of type DataCoords is not JSON serializable')
275
+ BuilderCoords: {"x": 0.0, "y": 0.0}
276
+ ```
277
+
278
+ ## What features does this have? ##
279
+
280
+ Included as an example implementation, the `slotclass` generator supports
281
+ `default_factory` for creating mutable defaults like lists, dicts etc.
282
+ It also supports default values that are not builtins (try this on
283
+ [Cluegen](https://github.com/dabeaz/cluegen)).
284
+
285
+ It will copy values provided as the `type` to `Field` into the
286
+ `__annotations__` dictionary of the class.
287
+ Values provided to `doc` will be placed in the final `__slots__`
288
+ field so they are present on the class if `help(...)` is called.
289
+
290
+ `AnnotationClass` offers the same features with additional methods of gathering
291
+ fields.
292
+
293
+ If you want something with more features you can look at the `prefab`
294
+ submodule which provides more specific features that differ further from the
295
+ behaviour of `dataclasses`.
296
+
297
+ ## Will you add \<feature\> to `classbuilder.prefab`? ##
298
+
299
+ No. Not unless it's something I need or find interesting.
300
+
301
+ The original version of `prefab_classes` was intended to have every feature
302
+ anybody could possibly require, but this is no longer the case with this
303
+ rebuilt version.
304
+
305
+ I will fix bugs (assuming they're not actually intended behaviour).
306
+
307
+ However the whole goal of this module is if you want to have a class generator
308
+ with a specific feature, you can create or add it yourself.
309
+
310
+ ## Credit ##
311
+
312
+ Heavily inspired by [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
313
+
314
+ [^1]: `SlotFields` is actually just a subclassed `dict` with no changes. `__slots__`
315
+ works with dictionaries using the values of the keys, while fields are normally
316
+ used for documentation.
317
+
318
+ [^2]: or `@attrs.define`.
@@ -0,0 +1,266 @@
1
+ # Ducktools: Class Builder #
2
+
3
+ `ducktools-classbuilder` is *the* Python package that will bring you the **joy**
4
+ of writing... functions... that will bring back the **joy** of writing classes.
5
+
6
+ Maybe.
7
+
8
+ While `attrs` and `dataclasses` are class boilerplate generators,
9
+ `ducktools.classbuilder` is intended to provide the tools to help make a customized
10
+ version of the same concept.
11
+
12
+ Install from PyPI with:
13
+ `python -m pip install ducktools-classbuilder`
14
+
15
+ ## Included Implementations ##
16
+
17
+ There are 2 different implementations provided with the module each of which offers
18
+ a subclass based and decorator based option.
19
+
20
+ > [!TIP]
21
+ > For more information on using these tools to create your own implementations
22
+ > using the builder see
23
+ > [the tutorial](https://ducktools-classbuilder.readthedocs.io/en/latest/tutorial.html)
24
+ > for a full tutorial and
25
+ > [extension_examples](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
26
+ > for other customizations.
27
+
28
+ ### Core ###
29
+
30
+ These tools are available from the main `ducktools.classbuilder` module.
31
+
32
+ * `@slotclass`
33
+ * A decorator based implementation that uses a special dict subclass assigned
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.
39
+
40
+ Each of these forms of class generation will result in the same methods being
41
+ attached to the class after the field information has been obtained.
42
+
43
+ ```python
44
+ from ducktools.classbuilder import Field, SlotFields, slotclass
45
+
46
+ @slotclass
47
+ class SlottedDC:
48
+ __slots__ = SlotFields(
49
+ the_answer=42,
50
+ the_question=Field(
51
+ default="What do you get if you multiply six by nine?",
52
+ doc="Life, the Universe, and Everything",
53
+ ),
54
+ )
55
+
56
+ ex = SlottedDC()
57
+ print(ex)
58
+ ```
59
+
60
+ ### Prefab ###
61
+
62
+ This is available from the `ducktools.classbuilder.prefab` submodule.
63
+
64
+ This includes more customization including `__prefab_pre_init__` and `__prefab_post_init__`
65
+ functions for subclass customization.
66
+
67
+ 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
+ will **not** be slotted and there is no `slots` argument to apply this.
71
+
72
+ Here is an example of applying a conversion in `__post_init__`:
73
+ ```python
74
+ from pathlib import Path
75
+ from ducktools.classbuilder.prefab import Prefab
76
+
77
+ class AppDetails(Prefab, frozen=True):
78
+ app_name: str
79
+ app_path: Path
80
+
81
+ def __prefab_post_init__(self, app_path: str | Path):
82
+ # frozen in `Prefab` is implemented as a 'set-once' __setattr__ function.
83
+ # So we do not need to use `object.__setattr__` here
84
+ self.app_path = Path(app_path)
85
+
86
+ steam = AppDetails(
87
+ "Steam",
88
+ r"C:\Program Files (x86)\Steam\steam.exe"
89
+ )
90
+
91
+ print(steam)
92
+ ```
93
+
94
+
95
+ ## What is the issue with generating `__slots__` with a decorator ##
96
+
97
+ If you want to use `__slots__` in order to save memory you have to declare
98
+ them when the class is originally created as you can't add them later.
99
+
100
+ When you use `@dataclass(slots=True)`[^2] with `dataclasses`, the function
101
+ has to make a new class and attempt to copy over everything from the original.
102
+
103
+ This is because decorators operate on classes *after they have been created*
104
+ while slots need to be declared beforehand.
105
+ While you can change the value of `__slots__` after a class has been created,
106
+ this will have no effect on the internal structure of the class.
107
+
108
+ By using a metaclass or by declaring fields using `__slots__` however,
109
+ the fields can be set *before* the class is constructed, so the class
110
+ will work correctly without needing to be rebuilt.
111
+
112
+ For example these two classes would be roughly equivalent, except that
113
+ `@dataclass` has had to recreate the class from scratch while `AnnotationClass`
114
+ has created `__slots__` and added the methods on to the original class.
115
+ This means that any references stored to the original class *before*
116
+ `@dataclass` has rebuilt the class will not be pointing towards the
117
+ correct class.
118
+
119
+ Here's a demonstration of the issue using a registry for serialization
120
+ functions.
121
+
122
+ > This example requires Python 3.10 or later as earlier versions of
123
+ > `dataclasses` did not support the `slots` argument.
124
+
125
+ ```python
126
+ import json
127
+ from dataclasses import dataclass
128
+ from ducktools.classbuilder import AnnotationClass, Field
129
+
130
+
131
+ class _RegisterDescriptor:
132
+ def __init__(self, func, registry):
133
+ self.func = func
134
+ self.registry = registry
135
+
136
+ def __set_name__(self, owner, name):
137
+ self.registry.register(owner, self.func)
138
+ setattr(owner, name, self.func)
139
+
140
+
141
+ class SerializeRegister:
142
+ def __init__(self):
143
+ self.serializers = {}
144
+
145
+ def register(self, cls, func):
146
+ self.serializers[cls] = func
147
+
148
+ def register_method(self, method):
149
+ return _RegisterDescriptor(method, self)
150
+
151
+ def default(self, o):
152
+ try:
153
+ return self.serializers[type(o)](o)
154
+ except KeyError:
155
+ raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
156
+
157
+
158
+ register = SerializeRegister()
159
+
160
+
161
+ @dataclass(slots=True)
162
+ class DataCoords:
163
+ x: float = 0.0
164
+ y: float = 0.0
165
+
166
+ @register.register_method
167
+ def to_json(self):
168
+ return {"x": self.x, "y": self.y}
169
+
170
+
171
+ # slots=True is the default for AnnotationClass
172
+ class BuilderCoords(AnnotationClass, slots=True):
173
+ x: float = 0.0
174
+ y: float = Field(default=0.0, doc="y coordinate")
175
+
176
+ @register.register_method
177
+ def to_json(self):
178
+ return {"x": self.x, "y": self.y}
179
+
180
+
181
+ # In both cases __slots__ have been defined
182
+ print(f"{DataCoords.__slots__ = }")
183
+ print(f"{BuilderCoords.__slots__ = }\n")
184
+
185
+ data_ex = DataCoords()
186
+ builder_ex = BuilderCoords()
187
+
188
+ objs = [data_ex, builder_ex]
189
+
190
+ print(data_ex)
191
+ print(builder_ex)
192
+ print()
193
+
194
+ # Demonstrate you can not set values not defined in slots
195
+ for obj in objs:
196
+ try:
197
+ obj.z = 1.0
198
+ except AttributeError as e:
199
+ print(e)
200
+ print()
201
+
202
+ print("Attempt to serialize:")
203
+ for obj in objs:
204
+ try:
205
+ print(f"{type(obj).__name__}: {json.dumps(obj, default=register.default)}")
206
+ except TypeError as e:
207
+ print(f"{type(obj).__name__}: {e!r}")
208
+ ```
209
+
210
+ Output (Python 3.12):
211
+ ```
212
+ DataCoords.__slots__ = ('x', 'y')
213
+ BuilderCoords.__slots__ = {'x': None, 'y': 'y coordinate'}
214
+
215
+ DataCoords(x=0.0, y=0.0)
216
+ BuilderCoords(x=0.0, y=0.0)
217
+
218
+ 'DataCoords' object has no attribute 'z'
219
+ 'BuilderCoords' object has no attribute 'z'
220
+
221
+ Attempt to serialize:
222
+ DataCoords: TypeError('Object of type DataCoords is not JSON serializable')
223
+ BuilderCoords: {"x": 0.0, "y": 0.0}
224
+ ```
225
+
226
+ ## What features does this have? ##
227
+
228
+ Included as an example implementation, the `slotclass` generator supports
229
+ `default_factory` for creating mutable defaults like lists, dicts etc.
230
+ It also supports default values that are not builtins (try this on
231
+ [Cluegen](https://github.com/dabeaz/cluegen)).
232
+
233
+ It will copy values provided as the `type` to `Field` into the
234
+ `__annotations__` dictionary of the class.
235
+ Values provided to `doc` will be placed in the final `__slots__`
236
+ field so they are present on the class if `help(...)` is called.
237
+
238
+ `AnnotationClass` offers the same features with additional methods of gathering
239
+ fields.
240
+
241
+ If you want something with more features you can look at the `prefab`
242
+ submodule which provides more specific features that differ further from the
243
+ behaviour of `dataclasses`.
244
+
245
+ ## Will you add \<feature\> to `classbuilder.prefab`? ##
246
+
247
+ No. Not unless it's something I need or find interesting.
248
+
249
+ The original version of `prefab_classes` was intended to have every feature
250
+ anybody could possibly require, but this is no longer the case with this
251
+ rebuilt version.
252
+
253
+ I will fix bugs (assuming they're not actually intended behaviour).
254
+
255
+ However the whole goal of this module is if you want to have a class generator
256
+ with a specific feature, you can create or add it yourself.
257
+
258
+ ## Credit ##
259
+
260
+ Heavily inspired by [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
261
+
262
+ [^1]: `SlotFields` is actually just a subclassed `dict` with no changes. `__slots__`
263
+ works with dictionaries using the values of the keys, while fields are normally
264
+ used for documentation.
265
+
266
+ [^2]: or `@attrs.define`.
@@ -14,10 +14,6 @@
14
14
  .. autofunction:: ducktools.classbuilder::slotclass
15
15
  ```
16
16
 
17
- ```{eval-rst}
18
- .. autofunction:: ducktools.classbuilder::annotationclass
19
- ```
20
-
21
17
 
22
18
  ## Builder functions and classes ##
23
19
 
@@ -42,9 +38,9 @@
42
38
  ```
43
39
 
44
40
  ```{eval-rst}
45
- .. autofunction:: ducktools.classbuilder::make_annotation_gatherer
41
+ .. autofunction:: ducktools.classbuilder.annotations::make_annotation_gatherer
46
42
  ```
47
43
 
48
44
  ```{eval-rst}
49
- .. autofunction:: ducktools.classbuilder::fieldclass
45
+ .. autoclass:: ducktools.classbuilder.annotations::AnnotationClass
50
46
  ```