ducktools-classbuilder 0.12.1__py3-none-any.whl
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.
- ducktools/classbuilder/__init__.py +1252 -0
- ducktools/classbuilder/__init__.pyi +283 -0
- ducktools/classbuilder/_version.py +2 -0
- ducktools/classbuilder/annotations/__init__.py +63 -0
- ducktools/classbuilder/annotations/annotations_314.py +104 -0
- ducktools/classbuilder/annotations/annotations_pre_314.py +42 -0
- ducktools/classbuilder/annotations.pyi +21 -0
- ducktools/classbuilder/prefab.py +884 -0
- ducktools/classbuilder/prefab.pyi +251 -0
- ducktools/classbuilder/py.typed +1 -0
- ducktools_classbuilder-0.12.1.dist-info/METADATA +335 -0
- ducktools_classbuilder-0.12.1.dist-info/RECORD +15 -0
- ducktools_classbuilder-0.12.1.dist-info/WHEEL +5 -0
- ducktools_classbuilder-0.12.1.dist-info/licenses/LICENSE +21 -0
- ducktools_classbuilder-0.12.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
from typing_extensions import dataclass_transform
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Suppress weird pylance error
|
|
7
|
+
from collections.abc import Callable # type: ignore
|
|
8
|
+
|
|
9
|
+
from . import (
|
|
10
|
+
NOTHING,
|
|
11
|
+
Field,
|
|
12
|
+
GeneratedCode,
|
|
13
|
+
MethodMaker,
|
|
14
|
+
SlotMakerMeta,
|
|
15
|
+
_SignatureMaker
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from . import SlotFields as SlotFields, KW_ONLY as KW_ONLY
|
|
19
|
+
|
|
20
|
+
# noinspection PyUnresolvedReferences
|
|
21
|
+
from . import _NothingType
|
|
22
|
+
|
|
23
|
+
PREFAB_FIELDS: str
|
|
24
|
+
PREFAB_INIT_FUNC: str
|
|
25
|
+
PRE_INIT_FUNC: str
|
|
26
|
+
POST_INIT_FUNC: str
|
|
27
|
+
|
|
28
|
+
_CopiableMappings = dict[str, typing.Any] | MappingProxyType[str, typing.Any]
|
|
29
|
+
|
|
30
|
+
class PrefabError(Exception): ...
|
|
31
|
+
|
|
32
|
+
def get_attributes(cls: type) -> dict[str, Attribute]: ...
|
|
33
|
+
|
|
34
|
+
def init_generator(cls: type, funcname: str = "__init__") -> GeneratedCode: ...
|
|
35
|
+
def iter_generator(cls: type, funcname: str = "__iter__") -> GeneratedCode: ...
|
|
36
|
+
def as_dict_generator(cls: type, funcname: str = "as_dict") -> GeneratedCode: ...
|
|
37
|
+
def hash_generator(cls: type, funcname: str = "__hash__") -> GeneratedCode: ...
|
|
38
|
+
|
|
39
|
+
init_maker: MethodMaker
|
|
40
|
+
prefab_init_maker: MethodMaker
|
|
41
|
+
repr_maker: MethodMaker
|
|
42
|
+
recursive_repr_maker: MethodMaker
|
|
43
|
+
eq_maker: MethodMaker
|
|
44
|
+
iter_maker: MethodMaker
|
|
45
|
+
asdict_maker: MethodMaker
|
|
46
|
+
hash_maker: MethodMaker
|
|
47
|
+
|
|
48
|
+
class Attribute(Field):
|
|
49
|
+
__slots__: dict
|
|
50
|
+
__signature__: _SignatureMaker
|
|
51
|
+
__classbuilder_gathered_fields__: tuple[dict[str, Field], dict[str, typing.Any]]
|
|
52
|
+
|
|
53
|
+
iter: bool
|
|
54
|
+
serialize: bool
|
|
55
|
+
metadata: dict
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
default: typing.Any | _NothingType = NOTHING,
|
|
61
|
+
default_factory: typing.Any | _NothingType = NOTHING,
|
|
62
|
+
type: type | _NothingType = NOTHING,
|
|
63
|
+
doc: str | None = ...,
|
|
64
|
+
init: bool = ...,
|
|
65
|
+
repr: bool = ...,
|
|
66
|
+
compare: bool = ...,
|
|
67
|
+
iter: bool = ...,
|
|
68
|
+
kw_only: bool = ...,
|
|
69
|
+
serialize: bool = ...,
|
|
70
|
+
metadata: dict | None = ...,
|
|
71
|
+
) -> None: ...
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str: ...
|
|
74
|
+
def __eq__(self, other: Attribute | object) -> bool: ...
|
|
75
|
+
def validate_field(self) -> None: ...
|
|
76
|
+
|
|
77
|
+
@typing.overload
|
|
78
|
+
def attribute(
|
|
79
|
+
*,
|
|
80
|
+
default: _T,
|
|
81
|
+
default_factory: _NothingType = NOTHING,
|
|
82
|
+
init: bool = ...,
|
|
83
|
+
repr: bool = ...,
|
|
84
|
+
compare: bool = ...,
|
|
85
|
+
iter: bool = ...,
|
|
86
|
+
kw_only: bool = ...,
|
|
87
|
+
serialize: bool = ...,
|
|
88
|
+
exclude_field: bool = ...,
|
|
89
|
+
private: bool = ...,
|
|
90
|
+
doc: str | None = ...,
|
|
91
|
+
metadata: dict | None = ...,
|
|
92
|
+
type: type | _NothingType = ...,
|
|
93
|
+
) -> _T: ...
|
|
94
|
+
|
|
95
|
+
@typing.overload
|
|
96
|
+
def attribute(
|
|
97
|
+
*,
|
|
98
|
+
default: _NothingType = NOTHING,
|
|
99
|
+
default_factory: Callable[[], _T],
|
|
100
|
+
init: bool = ...,
|
|
101
|
+
repr: bool = ...,
|
|
102
|
+
compare: bool = ...,
|
|
103
|
+
iter: bool = ...,
|
|
104
|
+
kw_only: bool = ...,
|
|
105
|
+
serialize: bool = ...,
|
|
106
|
+
exclude_field: bool = ...,
|
|
107
|
+
private: bool = ...,
|
|
108
|
+
doc: str | None = ...,
|
|
109
|
+
metadata: dict | None = ...,
|
|
110
|
+
type: type | _NothingType = ...,
|
|
111
|
+
) -> _T: ...
|
|
112
|
+
|
|
113
|
+
@typing.overload
|
|
114
|
+
def attribute(
|
|
115
|
+
*,
|
|
116
|
+
default: _NothingType = NOTHING,
|
|
117
|
+
default_factory: _NothingType = NOTHING,
|
|
118
|
+
init: bool = ...,
|
|
119
|
+
repr: bool = ...,
|
|
120
|
+
compare: bool = ...,
|
|
121
|
+
iter: bool = ...,
|
|
122
|
+
kw_only: bool = ...,
|
|
123
|
+
serialize: bool = ...,
|
|
124
|
+
exclude_field: bool = ...,
|
|
125
|
+
private: bool = ...,
|
|
126
|
+
doc: str | None = ...,
|
|
127
|
+
metadata: dict | None = ...,
|
|
128
|
+
type: type | _NothingType = ...,
|
|
129
|
+
) -> typing.Any: ...
|
|
130
|
+
|
|
131
|
+
def prefab_gatherer(cls_or_ns: type | MappingProxyType) -> tuple[dict[str, Attribute], dict[str, typing.Any]]: ...
|
|
132
|
+
|
|
133
|
+
def _make_prefab(
|
|
134
|
+
cls: type,
|
|
135
|
+
*,
|
|
136
|
+
init: bool = True,
|
|
137
|
+
repr: bool = True,
|
|
138
|
+
eq: bool = True,
|
|
139
|
+
iter: bool = False,
|
|
140
|
+
match_args: bool = True,
|
|
141
|
+
kw_only: bool = False,
|
|
142
|
+
frozen: bool = False,
|
|
143
|
+
replace: bool = True,
|
|
144
|
+
dict_method: bool = False,
|
|
145
|
+
recursive_repr: bool = False,
|
|
146
|
+
gathered_fields: Callable[[type], tuple[dict[str, Attribute], dict[str, typing.Any]]] | None = None,
|
|
147
|
+
ignore_annotations: bool = False,
|
|
148
|
+
) -> type: ...
|
|
149
|
+
|
|
150
|
+
_T = typing.TypeVar("_T")
|
|
151
|
+
|
|
152
|
+
# noinspection PyUnresolvedReferences
|
|
153
|
+
@dataclass_transform(field_specifiers=(Attribute, attribute))
|
|
154
|
+
class Prefab(metaclass=SlotMakerMeta):
|
|
155
|
+
__classbuilder_internals__: dict[str, typing.Any]
|
|
156
|
+
_meta_gatherer: Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]] = ...
|
|
157
|
+
__slots__: dict[str, typing.Any] = ...
|
|
158
|
+
def __init_subclass__(
|
|
159
|
+
cls,
|
|
160
|
+
*,
|
|
161
|
+
init: bool = True,
|
|
162
|
+
repr: bool = True,
|
|
163
|
+
eq: bool = True,
|
|
164
|
+
iter: bool = False,
|
|
165
|
+
match_args: bool = True,
|
|
166
|
+
kw_only: bool = False,
|
|
167
|
+
frozen: bool = False,
|
|
168
|
+
replace: bool = True,
|
|
169
|
+
dict_method: bool = False,
|
|
170
|
+
recursive_repr: bool = False,
|
|
171
|
+
) -> None: ...
|
|
172
|
+
|
|
173
|
+
# As far as I can tell these are the correct types
|
|
174
|
+
# But mypy.stubtest crashes trying to analyse them
|
|
175
|
+
# Due to the combination of overload and dataclass_transform
|
|
176
|
+
# @typing.overload
|
|
177
|
+
# def prefab(
|
|
178
|
+
# cls: None = None,
|
|
179
|
+
# *,
|
|
180
|
+
# init: bool = ...,
|
|
181
|
+
# repr: bool = ...,
|
|
182
|
+
# eq: bool = ...,
|
|
183
|
+
# iter: bool = ...,
|
|
184
|
+
# match_args: bool = ...,
|
|
185
|
+
# kw_only: bool = ...,
|
|
186
|
+
# frozen: bool = ...,
|
|
187
|
+
# dict_method: bool = ...,
|
|
188
|
+
# recursive_repr: bool = ...,
|
|
189
|
+
# ) -> Callable[[type[_T]], type[_T]]: ...
|
|
190
|
+
|
|
191
|
+
# @dataclass_transform(field_specifiers=(Attribute, attribute))
|
|
192
|
+
# @typing.overload
|
|
193
|
+
# def prefab(
|
|
194
|
+
# cls: type[_T],
|
|
195
|
+
# *,
|
|
196
|
+
# init: bool = ...,
|
|
197
|
+
# repr: bool = ...,
|
|
198
|
+
# eq: bool = ...,
|
|
199
|
+
# iter: bool = ...,
|
|
200
|
+
# match_args: bool = ...,
|
|
201
|
+
# kw_only: bool = ...,
|
|
202
|
+
# frozen: bool = ...,
|
|
203
|
+
# dict_method: bool = ...,
|
|
204
|
+
# recursive_repr: bool = ...,
|
|
205
|
+
# ) -> type[_T]: ...
|
|
206
|
+
|
|
207
|
+
# As mypy crashes, and the only difference is the return type
|
|
208
|
+
# just return `Any` for now to avoid the overload.
|
|
209
|
+
@dataclass_transform(field_specifiers=(Attribute, attribute))
|
|
210
|
+
def prefab(
|
|
211
|
+
cls: type[_T] | None = ...,
|
|
212
|
+
*,
|
|
213
|
+
init: bool = ...,
|
|
214
|
+
repr: bool = ...,
|
|
215
|
+
eq: bool = ...,
|
|
216
|
+
iter: bool = ...,
|
|
217
|
+
match_args: bool = ...,
|
|
218
|
+
kw_only: bool = ...,
|
|
219
|
+
frozen: bool = ...,
|
|
220
|
+
replace: bool = ...,
|
|
221
|
+
dict_method: bool = ...,
|
|
222
|
+
recursive_repr: bool = ...,
|
|
223
|
+
ignore_annotations: bool = ...,
|
|
224
|
+
) -> typing.Any: ...
|
|
225
|
+
|
|
226
|
+
def build_prefab(
|
|
227
|
+
class_name: str,
|
|
228
|
+
attributes: list[tuple[str, Attribute]],
|
|
229
|
+
*,
|
|
230
|
+
bases: tuple[type, ...] = (),
|
|
231
|
+
class_dict: dict[str, typing.Any] | None = None,
|
|
232
|
+
init: bool = True,
|
|
233
|
+
repr: bool = True,
|
|
234
|
+
eq: bool = True,
|
|
235
|
+
iter: bool = False,
|
|
236
|
+
match_args: bool = True,
|
|
237
|
+
kw_only: bool = False,
|
|
238
|
+
frozen: bool = False,
|
|
239
|
+
replace: bool = True,
|
|
240
|
+
dict_method: bool = False,
|
|
241
|
+
recursive_repr: bool = False,
|
|
242
|
+
slots: bool = False,
|
|
243
|
+
) -> type: ...
|
|
244
|
+
|
|
245
|
+
def is_prefab(o: typing.Any) -> bool: ...
|
|
246
|
+
|
|
247
|
+
def is_prefab_instance(o: object) -> bool: ...
|
|
248
|
+
|
|
249
|
+
def as_dict(o) -> dict[str, typing.Any]: ...
|
|
250
|
+
|
|
251
|
+
def replace(obj: _T, /, **changes: typing.Any) -> _T: ...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
partial
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ducktools-classbuilder
|
|
3
|
+
Version: 0.12.1
|
|
4
|
+
Summary: Toolkit for creating class boilerplate generators
|
|
5
|
+
Author: David C Ellis
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/davidcellis/ducktools-classbuilder
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: docs
|
|
19
|
+
Requires-Dist: sphinx>=8.1; extra == "docs"
|
|
20
|
+
Requires-Dist: myst-parser>=4.0; extra == "docs"
|
|
21
|
+
Requires-Dist: sphinx_rtd_theme>=3.0; extra == "docs"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Ducktools: Class Builder #
|
|
25
|
+
|
|
26
|
+
`ducktools-classbuilder` is *the* Python package that will bring you the **joy**
|
|
27
|
+
of writing... functions... that will bring back the **joy** of writing classes.
|
|
28
|
+
|
|
29
|
+
Maybe.
|
|
30
|
+
|
|
31
|
+
While `attrs` and `dataclasses` are class boilerplate generators,
|
|
32
|
+
`ducktools.classbuilder` is intended to provide the tools to help make a customized
|
|
33
|
+
version of the same concept.
|
|
34
|
+
|
|
35
|
+
Install from PyPI with:
|
|
36
|
+
`python -m pip install ducktools-classbuilder`
|
|
37
|
+
|
|
38
|
+
## Included Implementations ##
|
|
39
|
+
|
|
40
|
+
The classbuilder tools make up the core of this module and there is an implementation
|
|
41
|
+
using these tools in the `prefab` submodule.
|
|
42
|
+
|
|
43
|
+
There is also a minimal `@slotclass` example that can construct classes from a special
|
|
44
|
+
mapping used in `__slots__`.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from ducktools.classbuilder import Field, SlotFields, slotclass
|
|
48
|
+
|
|
49
|
+
@slotclass
|
|
50
|
+
class SlottedDC:
|
|
51
|
+
__slots__ = SlotFields(
|
|
52
|
+
the_answer=42,
|
|
53
|
+
the_question=Field(
|
|
54
|
+
default="What do you get if you multiply six by nine?",
|
|
55
|
+
doc="Life, the Universe, and Everything",
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
ex = SlottedDC()
|
|
60
|
+
print(ex)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Core ###
|
|
64
|
+
|
|
65
|
+
The core of the module provides tools for creating a customized version of the `dataclass` concept.
|
|
66
|
+
|
|
67
|
+
* `MethodMaker`
|
|
68
|
+
* This tool takes a function that generates source code and converts it into a descriptor
|
|
69
|
+
that will execute the source code and attach the gemerated method to a class on demand.
|
|
70
|
+
* `Field`
|
|
71
|
+
* This defines a basic dataclass-like field with some basic arguments
|
|
72
|
+
* This class itself is a dataclass-like of sorts
|
|
73
|
+
* Additional arguments can be added by subclassing and using annotations
|
|
74
|
+
* See `ducktools.classbuilder.prefab.Attribute` for an example of this
|
|
75
|
+
* Gatherers
|
|
76
|
+
* These collect field information and return both the gathered fields and any modifications
|
|
77
|
+
that will need to be made to the class when built to support them.
|
|
78
|
+
* `builder`
|
|
79
|
+
* This is the main tool used for constructing decorators and base classes to provide
|
|
80
|
+
generated methods.
|
|
81
|
+
* Other than the required changes to a class for `__slots__` that are done by `SlotMakerMeta`
|
|
82
|
+
this is where all class mutations should be applied.
|
|
83
|
+
* `SlotMakerMeta`
|
|
84
|
+
* When given a gatherer, this metaclass will create `__slots__` automatically.
|
|
85
|
+
|
|
86
|
+
> [!TIP]
|
|
87
|
+
> For more information on using these tools to create your own implementations
|
|
88
|
+
> using the builder see
|
|
89
|
+
> [the tutorial](https://ducktools-classbuilder.readthedocs.io/en/latest/tutorial.html)
|
|
90
|
+
> for a full tutorial and
|
|
91
|
+
> [extension_examples](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
|
|
92
|
+
> for other customizations.
|
|
93
|
+
|
|
94
|
+
### Prefab ###
|
|
95
|
+
|
|
96
|
+
This prebuilt implementation is available from the `ducktools.classbuilder.prefab` submodule.
|
|
97
|
+
|
|
98
|
+
This includes more customization including `__prefab_pre_init__` and `__prefab_post_init__`
|
|
99
|
+
functions for subclass customization.
|
|
100
|
+
|
|
101
|
+
A `@prefab` decorator and `Prefab` base class are provided.
|
|
102
|
+
|
|
103
|
+
`Prefab` will generate `__slots__` by default.
|
|
104
|
+
decorated classes with `@prefab` that do not declare fields using `__slots__`
|
|
105
|
+
will **not** be slotted and there is no `slots` argument to apply this.
|
|
106
|
+
|
|
107
|
+
Here is an example of applying a conversion in `__post_init__`:
|
|
108
|
+
```python
|
|
109
|
+
from pathlib import Path
|
|
110
|
+
from ducktools.classbuilder.prefab import Prefab
|
|
111
|
+
|
|
112
|
+
class AppDetails(Prefab, frozen=True):
|
|
113
|
+
app_name: str
|
|
114
|
+
app_path: Path
|
|
115
|
+
|
|
116
|
+
def __prefab_post_init__(self, app_path: str | Path):
|
|
117
|
+
# frozen in `Prefab` is implemented as a 'set-once' __setattr__ function.
|
|
118
|
+
# So we do not need to use `object.__setattr__` here
|
|
119
|
+
self.app_path = Path(app_path)
|
|
120
|
+
|
|
121
|
+
steam = AppDetails(
|
|
122
|
+
"Steam",
|
|
123
|
+
r"C:\Program Files (x86)\Steam\steam.exe"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
print(steam)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Features ####
|
|
130
|
+
|
|
131
|
+
`Prefab` and `@prefab` support many standard dataclass features along with
|
|
132
|
+
a few extras.
|
|
133
|
+
|
|
134
|
+
* All standard methods are generated on-demand
|
|
135
|
+
* This makes the construction of classes much faster in general
|
|
136
|
+
* Generation is done and then cached on first access
|
|
137
|
+
* Standard `__init__`, `__eq__` and `__repr__` methods are generated by default
|
|
138
|
+
- The `__repr__` implementation does not automatically protect against recursion,
|
|
139
|
+
but there is a `recursive_repr` argument that will do so if needed
|
|
140
|
+
* `repr`, `eq` and `kw_only` arguments work as they do in `dataclasses`
|
|
141
|
+
* There is an optional `iter` argument that will make the class iterable
|
|
142
|
+
* `__prefab_post_init__` will take any field name as an argument and can
|
|
143
|
+
be used to write a 'partial' `__init__` function for only non-standard attributes
|
|
144
|
+
* The `frozen` argument will make the dataclass a 'write once' object
|
|
145
|
+
* This is to make the partial `__prefab_post_init__` function more natural
|
|
146
|
+
to write for frozen classes
|
|
147
|
+
* `dict_method=True` will generate an `as_dict` method that gives a dictionary of
|
|
148
|
+
attributes that have `serialize=True` (the default)
|
|
149
|
+
* `ignore_annotations` can be used to only use the presence of `attribute` values
|
|
150
|
+
to decide how the class is constructed
|
|
151
|
+
* This is intended for cases where evaluating the annotations may trigger imports
|
|
152
|
+
which could be slow and unnecessary for the function of class generation
|
|
153
|
+
* `replace=False` can be used to avoid defining the `__replace__` method
|
|
154
|
+
* `attribute` has additional options over dataclasses' `Field`
|
|
155
|
+
* `iter=True` will include the attribute in the iterable if `__iter__` is generated
|
|
156
|
+
* `serialize=True` decides if the attribute is include in `as_dict`
|
|
157
|
+
* `exclude_field` is short for `repr=False`, `compare=False`, `iter=False`, `serialize=False`
|
|
158
|
+
* `private` is short for `exclude_field=True` and `init=False` and requires a default/factory
|
|
159
|
+
* `doc` will add this string as the value in slotted classes, which appears in `help()`
|
|
160
|
+
* `build_prefab` can be used to dynamically create classes and *does* support a slots argument
|
|
161
|
+
* Unlike dataclasses, this does not create the class twice in order to provide slots
|
|
162
|
+
|
|
163
|
+
There are also some intentionally missing features:
|
|
164
|
+
|
|
165
|
+
* The `@prefab` decorator does not and will not support a `slots` argument
|
|
166
|
+
* Use `Prefab` for slots.
|
|
167
|
+
* `as_dict` and the generated `.as_dict` method **do not** recurse or deep copy
|
|
168
|
+
* `unsafe_hash` is not provided
|
|
169
|
+
* `weakref_slot` is not available as an argument
|
|
170
|
+
* `__weakref__` can be added to slots by declaring it as if it were an attribute
|
|
171
|
+
* There is no check for mutable defaults
|
|
172
|
+
* You should still use `default_factory` as you would for dataclasses, not doing so
|
|
173
|
+
is still incorrect
|
|
174
|
+
* `dataclasses` uses hashability as a proxy for mutability, but technically this is
|
|
175
|
+
inaccurate as you can be unhashable but immutable and mutable but hashable
|
|
176
|
+
* This may change in a future version, but I haven't felt the need to add this check so far
|
|
177
|
+
* In Python 3.14 Annotations are gathered as `VALUE` if possible and `STRING` if this fails
|
|
178
|
+
* `VALUE` annotations are used as they are faster in most cases
|
|
179
|
+
* As the `__init__` method gets `__annotations__` these need to be either values or strings
|
|
180
|
+
to match the behaviour of previous Python versions
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
## What is the issue with generating `__slots__` with a decorator ##
|
|
184
|
+
|
|
185
|
+
If you want to use `__slots__` in order to save memory you have to declare
|
|
186
|
+
them when the class is originally created as you can't add them later.
|
|
187
|
+
|
|
188
|
+
When you use `@dataclass(slots=True)`[^2] with `dataclasses`, the function
|
|
189
|
+
has to make a new class and attempt to copy over everything from the original.
|
|
190
|
+
|
|
191
|
+
This is because decorators operate on classes *after they have been created*
|
|
192
|
+
while slots need to be declared beforehand.
|
|
193
|
+
While you can change the value of `__slots__` after a class has been created,
|
|
194
|
+
this will have no effect on the internal structure of the class.
|
|
195
|
+
|
|
196
|
+
By using a metaclass or by declaring fields using `__slots__` however,
|
|
197
|
+
the fields can be set *before* the class is constructed, so the class
|
|
198
|
+
will work correctly without needing to be rebuilt.
|
|
199
|
+
|
|
200
|
+
For example these two classes would be roughly equivalent, except that
|
|
201
|
+
`@dataclass` has had to recreate the class from scratch while `Prefab`
|
|
202
|
+
has created `__slots__` and added the methods on to the original class.
|
|
203
|
+
This means that any references stored to the original class *before*
|
|
204
|
+
`@dataclass` has rebuilt the class will not be pointing towards the
|
|
205
|
+
correct class.
|
|
206
|
+
|
|
207
|
+
Here's a demonstration of the issue using a registry for serialization
|
|
208
|
+
functions.
|
|
209
|
+
|
|
210
|
+
> This example requires Python 3.10 or later as earlier versions of
|
|
211
|
+
> `dataclasses` did not support the `slots` argument.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
import json
|
|
215
|
+
from dataclasses import dataclass
|
|
216
|
+
from ducktools.classbuilder.prefab import Prefab, attribute
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class _RegisterDescriptor:
|
|
220
|
+
def __init__(self, func, registry):
|
|
221
|
+
self.func = func
|
|
222
|
+
self.registry = registry
|
|
223
|
+
|
|
224
|
+
def __set_name__(self, owner, name):
|
|
225
|
+
self.registry.register(owner, self.func)
|
|
226
|
+
setattr(owner, name, self.func)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class SerializeRegister:
|
|
230
|
+
def __init__(self):
|
|
231
|
+
self.serializers = {}
|
|
232
|
+
|
|
233
|
+
def register(self, cls, func):
|
|
234
|
+
self.serializers[cls] = func
|
|
235
|
+
|
|
236
|
+
def register_method(self, method):
|
|
237
|
+
return _RegisterDescriptor(method, self)
|
|
238
|
+
|
|
239
|
+
def default(self, o):
|
|
240
|
+
try:
|
|
241
|
+
return self.serializers[type(o)](o)
|
|
242
|
+
except KeyError:
|
|
243
|
+
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
register = SerializeRegister()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass(slots=True)
|
|
250
|
+
class DataCoords:
|
|
251
|
+
x: float = 0.0
|
|
252
|
+
y: float = 0.0
|
|
253
|
+
|
|
254
|
+
@register.register_method
|
|
255
|
+
def to_json(self):
|
|
256
|
+
return {"x": self.x, "y": self.y}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# slots=True is the default for Prefab
|
|
260
|
+
class BuilderCoords(Prefab):
|
|
261
|
+
x: float = 0.0
|
|
262
|
+
y: float = attribute(default=0.0, doc="y coordinate")
|
|
263
|
+
|
|
264
|
+
@register.register_method
|
|
265
|
+
def to_json(self):
|
|
266
|
+
return {"x": self.x, "y": self.y}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# In both cases __slots__ have been defined
|
|
270
|
+
print(f"{DataCoords.__slots__ = }")
|
|
271
|
+
print(f"{BuilderCoords.__slots__ = }\n")
|
|
272
|
+
|
|
273
|
+
data_ex = DataCoords()
|
|
274
|
+
builder_ex = BuilderCoords()
|
|
275
|
+
|
|
276
|
+
objs = [data_ex, builder_ex]
|
|
277
|
+
|
|
278
|
+
print(data_ex)
|
|
279
|
+
print(builder_ex)
|
|
280
|
+
print()
|
|
281
|
+
|
|
282
|
+
# Demonstrate you can not set values not defined in slots
|
|
283
|
+
for obj in objs:
|
|
284
|
+
try:
|
|
285
|
+
obj.z = 1.0
|
|
286
|
+
except AttributeError as e:
|
|
287
|
+
print(e)
|
|
288
|
+
print()
|
|
289
|
+
|
|
290
|
+
print("Attempt to serialize:")
|
|
291
|
+
for obj in objs:
|
|
292
|
+
try:
|
|
293
|
+
print(f"{type(obj).__name__}: {json.dumps(obj, default=register.default)}")
|
|
294
|
+
except TypeError as e:
|
|
295
|
+
print(f"{type(obj).__name__}: {e!r}")
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Output (Python 3.12):
|
|
299
|
+
```
|
|
300
|
+
DataCoords.__slots__ = ('x', 'y')
|
|
301
|
+
BuilderCoords.__slots__ = {'x': None, 'y': 'y coordinate'}
|
|
302
|
+
|
|
303
|
+
DataCoords(x=0.0, y=0.0)
|
|
304
|
+
BuilderCoords(x=0.0, y=0.0)
|
|
305
|
+
|
|
306
|
+
'DataCoords' object has no attribute 'z'
|
|
307
|
+
'BuilderCoords' object has no attribute 'z'
|
|
308
|
+
|
|
309
|
+
Attempt to serialize:
|
|
310
|
+
DataCoords: TypeError('Object of type DataCoords is not JSON serializable')
|
|
311
|
+
BuilderCoords: {"x": 0.0, "y": 0.0}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Will you add \<feature\> to `classbuilder.prefab`? ##
|
|
315
|
+
|
|
316
|
+
No. Not unless it's something I need or find interesting.
|
|
317
|
+
|
|
318
|
+
The original version of `prefab_classes` was intended to have every feature
|
|
319
|
+
anybody could possibly require, but this is no longer the case with this
|
|
320
|
+
rebuilt version.
|
|
321
|
+
|
|
322
|
+
I will fix bugs (assuming they're not actually intended behaviour).
|
|
323
|
+
|
|
324
|
+
However the whole goal of this module is if you want to have a class generator
|
|
325
|
+
with a specific feature, you can create or add it yourself.
|
|
326
|
+
|
|
327
|
+
## Credit ##
|
|
328
|
+
|
|
329
|
+
Heavily inspired by [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
|
|
330
|
+
|
|
331
|
+
[^1]: `SlotFields` is actually just a subclassed `dict` with no changes. `__slots__`
|
|
332
|
+
works with dictionaries using the values of the keys, while fields are normally
|
|
333
|
+
used for documentation.
|
|
334
|
+
|
|
335
|
+
[^2]: or `@attrs.define`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ducktools/classbuilder/__init__.py,sha256=QtrTaV0nKA6qcZxBveucyj8UaO-q7_Mhv5Q8ek2LO6c,41551
|
|
2
|
+
ducktools/classbuilder/__init__.pyi,sha256=RBU83QZLZSp62XnPDcxTIAHu6U9o6vJs_Rq0XBhqE68,8377
|
|
3
|
+
ducktools/classbuilder/_version.py,sha256=8kcVJoc2ezPl65T7kB6_NJXlTK8tPiz-ZU6sBevSt8s,54
|
|
4
|
+
ducktools/classbuilder/annotations.pyi,sha256=bKTwQlPydbwrbVGaDu_PoSYOhuaqv8I_tMHf-g4aT0M,476
|
|
5
|
+
ducktools/classbuilder/prefab.py,sha256=XXH_NeDk6DhMfXHmr0nGmT_7zTbTg3RTcnSCg0Ujnvs,28375
|
|
6
|
+
ducktools/classbuilder/prefab.pyi,sha256=_cg9WsZqwkgOy0iowHtzGB2E6BSNJdKcdk0IC_Wz_qU,6756
|
|
7
|
+
ducktools/classbuilder/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
8
|
+
ducktools/classbuilder/annotations/__init__.py,sha256=n8VFmOItEbsMuq5gothp5yi6H3m3JJdVys04DVb0r-4,2145
|
|
9
|
+
ducktools/classbuilder/annotations/annotations_314.py,sha256=Yeng_kke1dcE6RarOzYN7SczWc9TSQEEZFEODzSj4uo,3692
|
|
10
|
+
ducktools/classbuilder/annotations/annotations_pre_314.py,sha256=Et-TYQYNVtMssShsS62PTu2mou2kr-ZJldYAAayhjAU,1651
|
|
11
|
+
ducktools_classbuilder-0.12.1.dist-info/licenses/LICENSE,sha256=6Thz9Dbw8R4fWInl6sGl8Rj3UnKnRbDwrc6jZerpugQ,1070
|
|
12
|
+
ducktools_classbuilder-0.12.1.dist-info/METADATA,sha256=uWB4nU2Gju2MOXTY30Mc-YsfwhLWEGGDuRfkQa-xJao,12347
|
|
13
|
+
ducktools_classbuilder-0.12.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
ducktools_classbuilder-0.12.1.dist-info/top_level.txt,sha256=uSDLtio3ZFqdwcsMJ2O5yhjB4Q3ytbBWbA8rJREganc,10
|
|
15
|
+
ducktools_classbuilder-0.12.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ducktools
|