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.
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/PKG-INFO +15 -15
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/README.md +14 -14
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/api.md +0 -4
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/extension_examples.md +2 -1
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/generated_code.md +16 -10
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/index.md +35 -6
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/tutorial.md +8 -2
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex9_annotated.py +0 -2
- ducktools_classbuilder-0.8.0/docs_code/index_example.py +34 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/tutorial_code.py +7 -4
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/__init__.py +2 -2
- ducktools_classbuilder-0.8.0/src/ducktools/classbuilder/_version.py +2 -0
- ducktools_classbuilder-0.8.0/src/ducktools/classbuilder/annotations.py +91 -0
- ducktools_classbuilder-0.8.0/src/ducktools/classbuilder/annotations.pyi +12 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/PKG-INFO +15 -15
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/SOURCES.txt +1 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/annotations/test_annotated.py +1 -1
- ducktools_classbuilder-0.8.0/tests/annotations/test_annotations_module.py +58 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/annotations/test_future_annotations.py +17 -3
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/py312_tests/test_generic_annotations.py +1 -1
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/test_slotmakermeta.py +1 -1
- ducktools_classbuilder-0.7.5/src/ducktools/classbuilder/_version.py +0 -2
- ducktools_classbuilder-0.7.5/src/ducktools/classbuilder/annotations.py +0 -223
- ducktools_classbuilder-0.7.5/src/ducktools/classbuilder/annotations.pyi +0 -27
- ducktools_classbuilder-0.7.5/tests/annotations/test_annotations_module.py +0 -118
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.github/dependabot.yml +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.github/workflows/auto_test.yml +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.github/workflows/publish_to_pypi.yml +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.github/workflows/publish_to_testpypi.yml +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.gitignore +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/.readthedocs.yaml +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/MANIFEST.in +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/Makefile +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/approach_vs_tool.md +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/conf.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/make.bat +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/perf/performance_tests.md +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs/prefab/index.md +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex10_frozen_attributes.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex1_basic.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex2_register.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex3_iterable.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex5_frozen.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex7_posonly.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/docs_code/docs_ex8_converters.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/perf/cluegen.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/perf/dataklasses.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/perf/hyperfine_testmaker.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/perf/perf_profile.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/pyproject.toml +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/setup.cfg +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/__init__.pyi +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/prefab.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/prefab.pyi +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/py.typed +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/conftest.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_construction.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_frozen.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_internals.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_private.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_slotted_class.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/dynamic/test_subclass_implementation.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/conftest.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/creation.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/creation_empty.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/dunders.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/hint_syntax.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/inheritance.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/init_ex.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/kw_only.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/examples/repr_func.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_creation.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_dunders.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_funcs.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_hint_syntax.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_inheritance.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_init.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_kw_only.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/prefab/shared/test_repr.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/py314_tests/test_forwardref_annotations.py +0 -0
- {ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/test_core.py +0 -0
- {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.
|
|
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
|
-
* `
|
|
90
|
-
* A
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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 `
|
|
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
|
|
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
|
|
226
|
-
class BuilderCoords(
|
|
228
|
+
# slots=True is the default for Prefab
|
|
229
|
+
class BuilderCoords(Prefab):
|
|
227
230
|
x: float = 0.0
|
|
228
|
-
y: float =
|
|
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
|
-
* `
|
|
36
|
-
* A
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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 `
|
|
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
|
|
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
|
|
172
|
-
class BuilderCoords(
|
|
174
|
+
# slots=True is the default for Prefab
|
|
175
|
+
class BuilderCoords(Prefab):
|
|
173
176
|
x: float = 0.0
|
|
174
|
-
y: float =
|
|
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`.
|
|
@@ -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
|
-
|
|
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
|
|
10
|
+
from ducktools.classbuilder import SlotFields, get_methods, slotclass
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
##
|
|
68
|
+
## Using Annotations ##
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
|
149
|
+
We can take a quick look at what this generates by applying it to a `slotclass`:
|
|
150
150
|
```python
|
|
151
|
-
|
|
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"
|
|
@@ -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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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)
|
{ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/src/ducktools/classbuilder/__init__.py
RENAMED
|
@@ -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
|
|
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,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.
|
|
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
|
-
* `
|
|
90
|
-
* A
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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 `
|
|
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
|
|
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
|
|
226
|
-
class BuilderCoords(
|
|
228
|
+
# slots=True is the default for Prefab
|
|
229
|
+
class BuilderCoords(Prefab):
|
|
227
230
|
x: float = 0.0
|
|
228
|
-
y: float =
|
|
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`.
|
{ducktools_classbuilder-0.7.5 → ducktools_classbuilder-0.8.0}/tests/annotations/test_annotated.py
RENAMED
|
@@ -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":
|
|
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":
|
|
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__)
|