ducktools-classbuilder 0.6.0__tar.gz → 0.6.2__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.6.0/src/ducktools_classbuilder.egg-info → ducktools_classbuilder-0.6.2}/PKG-INFO +1 -1
- ducktools_classbuilder-0.6.2/docs/approach_vs_tool.md +30 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/extension_examples.md +8 -8
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/tutorial.md +2 -2
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/__init__.py +70 -25
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/__init__.pyi +25 -11
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/annotations.pyi +1 -1
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/prefab.py +160 -167
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/prefab.pyi +7 -6
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2/src/ducktools_classbuilder.egg-info}/PKG-INFO +1 -1
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/test_core.py +33 -2
- ducktools_classbuilder-0.6.0/docs/approach_vs_tool.md +0 -16
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/MANIFEST.in +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/README.md +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/Makefile +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/api.md +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/conf.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/generated_code.md +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/index.md +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/make.bat +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/perf/performance_tests.md +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/prefab/index.md +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/pyproject.toml +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/setup.cfg +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/annotations.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/py.typed +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools_classbuilder.egg-info/SOURCES.txt +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/annotations/test_annotated.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/annotations/test_annotations_module.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/conftest.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_construction.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_internals.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_slotted_class.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_subclass_implementation.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/conftest.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/creation.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/creation_empty.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/dunders.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/frozen_prefabs.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/hint_syntax.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/inheritance.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/init_ex.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/kw_only.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/examples/repr_func.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_creation.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_dunders.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_frozen.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_funcs.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_hint_syntax.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_inheritance.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_init.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_kw_only.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_repr.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/py312_tests/test_generic_annotations.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/test_field_flags.py +0 -0
- {ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/test_slotmakermeta.py +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Approaches and tools #
|
|
2
|
+
|
|
3
|
+
As this module's code generation is inspired by the workings of [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
|
|
4
|
+
I thought it was briefly worth discussing his note on learning an approach vs using a tool.
|
|
5
|
+
|
|
6
|
+
I think that learning an approach is valuable, this module would not exist without the
|
|
7
|
+
example given by `cluegen`.
|
|
8
|
+
|
|
9
|
+
However, what I found was that in essentially every case where I wanted to use
|
|
10
|
+
these generating tools, I needed to modify them - often significantly.
|
|
11
|
+
It quickly became easier to just create my own tool and upload it as a package.
|
|
12
|
+
|
|
13
|
+
For example, `cluegen` has a few subtle "exercises for the reader". It needs extending
|
|
14
|
+
and fixing for some use-cases.
|
|
15
|
+
* Default values that are not builtins need to be passed as part of the globals
|
|
16
|
+
dict to `exec`.
|
|
17
|
+
* No support for mutable defaults.
|
|
18
|
+
* Subclass methods will be overwritten if they call a cluegen method that has not been
|
|
19
|
+
generated via `super().methodname(...)`
|
|
20
|
+
* `inspect.signature(cls)` does not work if `cls.__init__` has not already been generated.
|
|
21
|
+
(I think this is actually a bug in inspect).
|
|
22
|
+
* Need an extra filter to support things like `ClassVar`.
|
|
23
|
+
|
|
24
|
+
In the general spirit though, this module intends to provide some basic tools to help
|
|
25
|
+
create your own customized boilerplate generators.
|
|
26
|
+
The generator included in the base module is intended to be used to help 'bootstrap' a
|
|
27
|
+
modified generator with features that work how **you** want them to work.
|
|
28
|
+
|
|
29
|
+
The `prefab` module is the more fully featured tool that handles the additional cases *I*
|
|
30
|
+
needed.
|
|
@@ -79,12 +79,12 @@ from ducktools.classbuilder import (
|
|
|
79
79
|
)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
def iter_generator(cls):
|
|
82
|
+
def iter_generator(cls, funcname="__iter__"):
|
|
83
83
|
field_names = get_fields(cls).keys()
|
|
84
84
|
field_yield = "\n".join(f" yield self.{f}" for f in field_names)
|
|
85
85
|
if not field_yield:
|
|
86
86
|
field_yield = " yield from ()"
|
|
87
|
-
code = f"def
|
|
87
|
+
code = f"def {funcname}(self):\n" f"{field_yield}"
|
|
88
88
|
globs = {}
|
|
89
89
|
return GeneratedCode(code, globs)
|
|
90
90
|
|
|
@@ -143,7 +143,7 @@ class PosOnlyField(Field):
|
|
|
143
143
|
__slots__ = SlotFields(pos_only=True)
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
def init_generator(cls):
|
|
146
|
+
def init_generator(cls, funcname="__init__"):
|
|
147
147
|
fields = get_fields(cls)
|
|
148
148
|
|
|
149
149
|
arglist = []
|
|
@@ -177,11 +177,11 @@ def init_generator(cls):
|
|
|
177
177
|
|
|
178
178
|
args = ", ".join(arglist)
|
|
179
179
|
assigns = "\n ".join(assignments)
|
|
180
|
-
code = f"def
|
|
180
|
+
code = f"def {funcname}(self, {args}):\n" f" {assigns}\n"
|
|
181
181
|
return GeneratedCode(code, globs)
|
|
182
182
|
|
|
183
183
|
|
|
184
|
-
def repr_generator(cls):
|
|
184
|
+
def repr_generator(cls, funcname="__repr__"):
|
|
185
185
|
fields = get_fields(cls)
|
|
186
186
|
content_list = []
|
|
187
187
|
for name, field in fields.items():
|
|
@@ -193,7 +193,7 @@ def repr_generator(cls):
|
|
|
193
193
|
|
|
194
194
|
content = ", ".join(content_list)
|
|
195
195
|
code = (
|
|
196
|
-
f"def
|
|
196
|
+
f"def {funcname}(self):\n"
|
|
197
197
|
f" return f'{{type(self).__qualname__}}({content})'\n"
|
|
198
198
|
)
|
|
199
199
|
globs = {}
|
|
@@ -281,7 +281,7 @@ class ConverterField(Field):
|
|
|
281
281
|
converter = Field(default=None)
|
|
282
282
|
|
|
283
283
|
|
|
284
|
-
def setattr_generator(cls):
|
|
284
|
+
def setattr_generator(cls, funcname="__setattr__"):
|
|
285
285
|
fields = get_fields(cls)
|
|
286
286
|
converters = {}
|
|
287
287
|
for k, v in fields.items():
|
|
@@ -294,7 +294,7 @@ def setattr_generator(cls):
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
code = (
|
|
297
|
-
f"def
|
|
297
|
+
f"def {funcname}(self, name, value):\n"
|
|
298
298
|
f" if conv := _converters.get(name):\n"
|
|
299
299
|
f" _object_setattr(self, name, conv(value))\n"
|
|
300
300
|
f" else:\n"
|
|
@@ -120,7 +120,7 @@ field_3: <HIDDEN>
|
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
```python
|
|
123
|
-
def report_generator(cls):
|
|
123
|
+
def report_generator(cls, funcname="report"):
|
|
124
124
|
fields = dtbuild.get_fields(cls)
|
|
125
125
|
|
|
126
126
|
field_reports = []
|
|
@@ -135,7 +135,7 @@ def report_generator(cls):
|
|
|
135
135
|
|
|
136
136
|
code = (
|
|
137
137
|
"@property\n"
|
|
138
|
-
"def
|
|
138
|
+
f"def {funcname}(self):\n"
|
|
139
139
|
f" return f\"{class_str}\\n{reports_str}\""
|
|
140
140
|
)
|
|
141
141
|
globs = {}
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/__init__.py
RENAMED
|
@@ -34,7 +34,7 @@ import sys
|
|
|
34
34
|
|
|
35
35
|
from .annotations import get_ns_annotations, is_classvar
|
|
36
36
|
|
|
37
|
-
__version__ = "v0.6.
|
|
37
|
+
__version__ = "v0.6.2"
|
|
38
38
|
|
|
39
39
|
# Change this name if you make heavy modifications
|
|
40
40
|
INTERNALS_DICT = "__classbuilder_internals__"
|
|
@@ -158,36 +158,73 @@ class MethodMaker:
|
|
|
158
158
|
def __repr__(self):
|
|
159
159
|
return f"<MethodMaker for {self.funcname!r} method>"
|
|
160
160
|
|
|
161
|
-
def __get__(self,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
def __get__(self, inst, cls):
|
|
162
|
+
local_vars = {}
|
|
163
|
+
|
|
164
|
+
# This can be called via super().funcname(...) in which case the class
|
|
165
|
+
# may not be the correct one. If this is the correct class
|
|
166
|
+
# it should have this descriptor in the class dict under
|
|
167
|
+
# the correct funcname.
|
|
168
|
+
# Otherwise is should be found in the MRO of the class.
|
|
169
|
+
if cls.__dict__.get(self.funcname) is self:
|
|
170
|
+
gen_cls = cls
|
|
165
171
|
else:
|
|
166
|
-
#
|
|
167
|
-
|
|
172
|
+
for c in cls.__mro__[1:]: # skip 'cls' as special cased
|
|
173
|
+
if c.__dict__.get(self.funcname) is self:
|
|
174
|
+
gen_cls = c
|
|
175
|
+
break
|
|
176
|
+
else: # pragma: no cover
|
|
177
|
+
# This should only be reached if called with incorrect arguments
|
|
178
|
+
# manually
|
|
179
|
+
raise AttributeError(
|
|
180
|
+
f"Could not find {self!r} in class {cls.__name__!r} MRO."
|
|
181
|
+
)
|
|
168
182
|
|
|
169
|
-
|
|
170
|
-
gen = self.code_generator(cls)
|
|
183
|
+
gen = self.code_generator(gen_cls, self.funcname)
|
|
171
184
|
exec(gen.source_code, gen.globs, local_vars)
|
|
172
185
|
method = local_vars.get(self.funcname)
|
|
173
186
|
|
|
174
187
|
try:
|
|
175
|
-
method.__qualname__ = f"{
|
|
188
|
+
method.__qualname__ = f"{gen_cls.__qualname__}.{self.funcname}"
|
|
176
189
|
except AttributeError:
|
|
177
190
|
# This might be a property or some other special
|
|
178
191
|
# descriptor. Don't try to rename.
|
|
179
192
|
pass
|
|
180
193
|
|
|
181
194
|
# Replace this descriptor on the class with the generated function
|
|
182
|
-
setattr(
|
|
195
|
+
setattr(gen_cls, self.funcname, method)
|
|
183
196
|
|
|
184
197
|
# Use 'get' to return the generated function as a bound method
|
|
185
198
|
# instead of as a regular function for first usage.
|
|
186
|
-
return method.__get__(
|
|
199
|
+
return method.__get__(inst, cls)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class _SignatureMaker:
|
|
203
|
+
# 'inspect.signature' calls the `__get__` method of the `__init__` methodmaker with
|
|
204
|
+
# the wrong arguments.
|
|
205
|
+
# Instead of __get__(None, cls) or __get__(inst, type(inst))
|
|
206
|
+
# it uses __get__(cls, type(cls)).
|
|
207
|
+
#
|
|
208
|
+
# If this is done before `__init__` has been generated then
|
|
209
|
+
# help(cls) will fail along with inspect.signature(cls)
|
|
210
|
+
# This signature maker descriptor is placed to override __signature__ and force
|
|
211
|
+
# the `__init__` signature to be generated first if the signature is requested.
|
|
212
|
+
def __get__(self, instance, cls):
|
|
213
|
+
import inspect # Deferred inspect import
|
|
214
|
+
_ = cls.__init__ # force generation of `__init__` function
|
|
215
|
+
# Remove this attribute from the class
|
|
216
|
+
# This prevents recursion back into this __get__ method.
|
|
217
|
+
delattr(cls, "__signature__")
|
|
218
|
+
sig = inspect.signature(cls)
|
|
219
|
+
setattr(cls, "__signature__", sig)
|
|
220
|
+
return sig
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
signature_maker = _SignatureMaker()
|
|
187
224
|
|
|
188
225
|
|
|
189
226
|
def get_init_generator(null=NOTHING, extra_code=None):
|
|
190
|
-
def cls_init_maker(cls):
|
|
227
|
+
def cls_init_maker(cls, funcname="__init__"):
|
|
191
228
|
fields = get_fields(cls)
|
|
192
229
|
flags = get_flags(cls)
|
|
193
230
|
|
|
@@ -239,7 +276,7 @@ def get_init_generator(null=NOTHING, extra_code=None):
|
|
|
239
276
|
|
|
240
277
|
assigns = "\n ".join(assignments) if assignments else "pass\n"
|
|
241
278
|
code = (
|
|
242
|
-
f"def
|
|
279
|
+
f"def {funcname}(self, {args}):\n"
|
|
243
280
|
f" {assigns}\n"
|
|
244
281
|
)
|
|
245
282
|
# Handle additional function calls
|
|
@@ -265,7 +302,7 @@ def get_repr_generator(recursion_safe=False, eval_safe=False):
|
|
|
265
302
|
not evaluate.
|
|
266
303
|
:return:
|
|
267
304
|
"""
|
|
268
|
-
def cls_repr_generator(cls):
|
|
305
|
+
def cls_repr_generator(cls, funcname="__repr__"):
|
|
269
306
|
fields = get_fields(cls)
|
|
270
307
|
|
|
271
308
|
globs = {}
|
|
@@ -295,19 +332,19 @@ def get_repr_generator(recursion_safe=False, eval_safe=False):
|
|
|
295
332
|
if content:
|
|
296
333
|
code = (
|
|
297
334
|
f"{recursion_func}"
|
|
298
|
-
f"def
|
|
335
|
+
f"def {funcname}(self):\n"
|
|
299
336
|
f" return f'<generated class {{type(self).__qualname__}}; {content}>'\n"
|
|
300
337
|
)
|
|
301
338
|
else:
|
|
302
339
|
code = (
|
|
303
340
|
f"{recursion_func}"
|
|
304
|
-
f"def
|
|
341
|
+
f"def {funcname}(self):\n"
|
|
305
342
|
f" return f'<generated class {{type(self).__qualname__}}>'\n"
|
|
306
343
|
)
|
|
307
344
|
else:
|
|
308
345
|
code = (
|
|
309
346
|
f"{recursion_func}"
|
|
310
|
-
f"def
|
|
347
|
+
f"def {funcname}(self):\n"
|
|
311
348
|
f" return f'{{type(self).__qualname__}}({content})'\n"
|
|
312
349
|
)
|
|
313
350
|
|
|
@@ -318,7 +355,7 @@ def get_repr_generator(recursion_safe=False, eval_safe=False):
|
|
|
318
355
|
repr_generator = get_repr_generator()
|
|
319
356
|
|
|
320
357
|
|
|
321
|
-
def eq_generator(cls):
|
|
358
|
+
def eq_generator(cls, funcname="__eq__"):
|
|
322
359
|
class_comparison = "self.__class__ is other.__class__"
|
|
323
360
|
field_names = [
|
|
324
361
|
name
|
|
@@ -334,7 +371,7 @@ def eq_generator(cls):
|
|
|
334
371
|
instance_comparison = "True"
|
|
335
372
|
|
|
336
373
|
code = (
|
|
337
|
-
f"def
|
|
374
|
+
f"def {funcname}(self, other):\n"
|
|
338
375
|
f" return {instance_comparison} if {class_comparison} else NotImplemented\n"
|
|
339
376
|
)
|
|
340
377
|
globs = {}
|
|
@@ -342,7 +379,7 @@ def eq_generator(cls):
|
|
|
342
379
|
return GeneratedCode(code, globs)
|
|
343
380
|
|
|
344
381
|
|
|
345
|
-
def frozen_setattr_generator(cls):
|
|
382
|
+
def frozen_setattr_generator(cls, funcname="__setattr__"):
|
|
346
383
|
globs = {}
|
|
347
384
|
field_names = set(get_fields(cls))
|
|
348
385
|
flags = get_flags(cls)
|
|
@@ -366,19 +403,19 @@ def frozen_setattr_generator(cls):
|
|
|
366
403
|
f" else:\n"
|
|
367
404
|
f" {setattr_method}\n"
|
|
368
405
|
)
|
|
369
|
-
code = f"def
|
|
406
|
+
code = f"def {funcname}(self, name, value):\n{body}"
|
|
370
407
|
|
|
371
408
|
return GeneratedCode(code, globs)
|
|
372
409
|
|
|
373
410
|
|
|
374
|
-
def frozen_delattr_generator(cls):
|
|
411
|
+
def frozen_delattr_generator(cls, funcname="__delattr__"):
|
|
375
412
|
body = (
|
|
376
413
|
' raise TypeError(\n'
|
|
377
414
|
' f"{type(self).__name__!r} object "\n'
|
|
378
415
|
' f"does not support attribute deletion"\n'
|
|
379
416
|
' )\n'
|
|
380
417
|
)
|
|
381
|
-
code = f"def
|
|
418
|
+
code = f"def {funcname}(self, name):\n{body}"
|
|
382
419
|
globs = {}
|
|
383
420
|
return GeneratedCode(code, globs)
|
|
384
421
|
|
|
@@ -402,7 +439,7 @@ _field_init_maker = MethodMaker(
|
|
|
402
439
|
)
|
|
403
440
|
|
|
404
441
|
|
|
405
|
-
def builder(cls=None, /, *, gatherer, methods, flags=None):
|
|
442
|
+
def builder(cls=None, /, *, gatherer, methods, flags=None, fix_signature=True):
|
|
406
443
|
"""
|
|
407
444
|
The main builder for class generation
|
|
408
445
|
|
|
@@ -413,6 +450,10 @@ def builder(cls=None, /, *, gatherer, methods, flags=None):
|
|
|
413
450
|
:type methods: set[MethodMaker]
|
|
414
451
|
:param flags: additional flags to store in the internals dictionary
|
|
415
452
|
for use by method generators.
|
|
453
|
+
:type flags: None | dict[str, bool]
|
|
454
|
+
:param fix_signature: Add a __signature__ attribute to work-around an issue with
|
|
455
|
+
inspect.signature incorrectly handling __init__ descriptors.
|
|
456
|
+
:type fix_signature: bool
|
|
416
457
|
:return: The modified class (the class itself is modified, but this is expected).
|
|
417
458
|
"""
|
|
418
459
|
# Handle `None` to make wrapping with a decorator easier.
|
|
@@ -459,6 +500,10 @@ def builder(cls=None, /, *, gatherer, methods, flags=None):
|
|
|
459
500
|
|
|
460
501
|
internals["methods"] = _MappingProxyType(internal_methods)
|
|
461
502
|
|
|
503
|
+
# Fix for inspect.signature(cls)
|
|
504
|
+
if fix_signature:
|
|
505
|
+
setattr(cls, "__signature__", signature_maker)
|
|
506
|
+
|
|
462
507
|
return cls
|
|
463
508
|
|
|
464
509
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import types
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
|
+
import inspect
|
|
5
|
+
|
|
4
6
|
from collections.abc import Callable
|
|
5
7
|
from types import MappingProxyType
|
|
6
8
|
from typing_extensions import dataclass_transform
|
|
@@ -30,7 +32,10 @@ class _KW_ONLY_TYPE:
|
|
|
30
32
|
|
|
31
33
|
KW_ONLY: _KW_ONLY_TYPE
|
|
32
34
|
# Stub Only
|
|
33
|
-
|
|
35
|
+
@typing.type_check_only
|
|
36
|
+
class _CodegenType(typing.Protocol):
|
|
37
|
+
def __call__(self, cls: type, funcname: str = ...) -> GeneratedCode: ...
|
|
38
|
+
|
|
34
39
|
|
|
35
40
|
class GeneratedCode:
|
|
36
41
|
__slots__: tuple[str, str]
|
|
@@ -43,28 +48,33 @@ class GeneratedCode:
|
|
|
43
48
|
|
|
44
49
|
class MethodMaker:
|
|
45
50
|
funcname: str
|
|
46
|
-
code_generator:
|
|
47
|
-
def __init__(self, funcname: str, code_generator:
|
|
51
|
+
code_generator: _CodegenType
|
|
52
|
+
def __init__(self, funcname: str, code_generator: _CodegenType) -> None: ...
|
|
48
53
|
def __repr__(self) -> str: ...
|
|
49
|
-
def __get__(self, instance, cls
|
|
54
|
+
def __get__(self, instance, cls) -> Callable: ...
|
|
55
|
+
|
|
56
|
+
class _SignatureMaker:
|
|
57
|
+
def __get__(self, instance, cls) -> inspect.Signature: ...
|
|
58
|
+
|
|
59
|
+
signature_maker: _SignatureMaker
|
|
50
60
|
|
|
51
61
|
def get_init_generator(
|
|
52
62
|
null: _NothingType = NOTHING,
|
|
53
63
|
extra_code: None | list[str] = None
|
|
54
|
-
) ->
|
|
64
|
+
) -> _CodegenType: ...
|
|
55
65
|
|
|
56
|
-
def init_generator(cls: type) -> GeneratedCode: ...
|
|
66
|
+
def init_generator(cls: type, funcname: str="__init__") -> GeneratedCode: ...
|
|
57
67
|
|
|
58
68
|
def get_repr_generator(
|
|
59
69
|
recursion_safe: bool = False,
|
|
60
70
|
eval_safe: bool = False
|
|
61
|
-
) ->
|
|
62
|
-
def repr_generator(cls: type) -> GeneratedCode: ...
|
|
63
|
-
def eq_generator(cls: type) -> GeneratedCode: ...
|
|
71
|
+
) -> _CodegenType: ...
|
|
72
|
+
def repr_generator(cls: type, funcname: str = "__repr__") -> GeneratedCode: ...
|
|
73
|
+
def eq_generator(cls: type, funcname: str = "__eq__") -> GeneratedCode: ...
|
|
64
74
|
|
|
65
|
-
def frozen_setattr_generator(cls: type) -> GeneratedCode: ...
|
|
75
|
+
def frozen_setattr_generator(cls: type, funcname: str = "__setattr__") -> GeneratedCode: ...
|
|
66
76
|
|
|
67
|
-
def frozen_delattr_generator(cls: type) -> GeneratedCode: ...
|
|
77
|
+
def frozen_delattr_generator(cls: type, funcname: str = "__delattr__") -> GeneratedCode: ...
|
|
68
78
|
|
|
69
79
|
init_maker: MethodMaker
|
|
70
80
|
repr_maker: MethodMaker
|
|
@@ -83,6 +93,7 @@ def builder(
|
|
|
83
93
|
gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]],
|
|
84
94
|
methods: frozenset[MethodMaker] | set[MethodMaker],
|
|
85
95
|
flags: dict[str, bool] | None = None,
|
|
96
|
+
fix_signature: bool = ...,
|
|
86
97
|
) -> type[_T]: ...
|
|
87
98
|
|
|
88
99
|
@typing.overload
|
|
@@ -93,6 +104,7 @@ def builder(
|
|
|
93
104
|
gatherer: Callable[[type], tuple[dict[str, Field], dict[str, typing.Any]]],
|
|
94
105
|
methods: frozenset[MethodMaker] | set[MethodMaker],
|
|
95
106
|
flags: dict[str, bool] | None = None,
|
|
107
|
+
fix_signature: bool = ...,
|
|
96
108
|
) -> Callable[[type[_T]], type[_T]]: ...
|
|
97
109
|
|
|
98
110
|
|
|
@@ -123,6 +135,7 @@ class Field(metaclass=SlotMakerMeta):
|
|
|
123
135
|
|
|
124
136
|
__slots__: dict[str, str]
|
|
125
137
|
__classbuilder_internals__: dict
|
|
138
|
+
__signature__: inspect.Signature
|
|
126
139
|
|
|
127
140
|
def __init__(
|
|
128
141
|
self,
|
|
@@ -246,6 +259,7 @@ class GatheredFields:
|
|
|
246
259
|
modifications: dict[str, typing.Any]
|
|
247
260
|
|
|
248
261
|
__classbuilder_internals__: dict
|
|
262
|
+
__signature__: inspect.Signature
|
|
249
263
|
|
|
250
264
|
def __init__(
|
|
251
265
|
self,
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/prefab.py
RENAMED
|
@@ -60,202 +60,195 @@ def get_attributes(cls):
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
# Method Generators
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
flags = get_flags(cls)
|
|
69
|
-
|
|
70
|
-
kw_only = flags.get("kw_only", False)
|
|
71
|
-
|
|
72
|
-
# Handle pre/post init first - post_init can change types for __init__
|
|
73
|
-
# Get pre and post init arguments
|
|
74
|
-
pre_init_args = []
|
|
75
|
-
post_init_args = []
|
|
76
|
-
post_init_annotations = {}
|
|
77
|
-
|
|
78
|
-
for func_name, func_arglist in [
|
|
79
|
-
(PRE_INIT_FUNC, pre_init_args),
|
|
80
|
-
(POST_INIT_FUNC, post_init_args),
|
|
81
|
-
]:
|
|
82
|
-
try:
|
|
83
|
-
func = getattr(cls, func_name)
|
|
84
|
-
func_code = func.__code__
|
|
85
|
-
except AttributeError:
|
|
86
|
-
pass
|
|
87
|
-
else:
|
|
88
|
-
argcount = func_code.co_argcount + func_code.co_kwonlyargcount
|
|
63
|
+
def init_generator(cls, funcname="__init__"):
|
|
64
|
+
globs = {}
|
|
65
|
+
# Get the internals dictionary and prepare attributes
|
|
66
|
+
attributes = get_attributes(cls)
|
|
67
|
+
flags = get_flags(cls)
|
|
89
68
|
|
|
90
|
-
|
|
91
|
-
is_static = type(cls.__dict__.get(func_name)) is staticmethod
|
|
69
|
+
kw_only = flags.get("kw_only", False)
|
|
92
70
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
71
|
+
# Handle pre/post init first - post_init can change types for __init__
|
|
72
|
+
# Get pre and post init arguments
|
|
73
|
+
pre_init_args = []
|
|
74
|
+
post_init_args = []
|
|
75
|
+
post_init_annotations = {}
|
|
76
|
+
|
|
77
|
+
for extra_funcname, func_arglist in [
|
|
78
|
+
(PRE_INIT_FUNC, pre_init_args),
|
|
79
|
+
(POST_INIT_FUNC, post_init_args),
|
|
80
|
+
]:
|
|
81
|
+
try:
|
|
82
|
+
func = getattr(cls, extra_funcname)
|
|
83
|
+
func_code = func.__code__
|
|
84
|
+
except AttributeError:
|
|
85
|
+
pass
|
|
86
|
+
else:
|
|
87
|
+
argcount = func_code.co_argcount + func_code.co_kwonlyargcount
|
|
98
88
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
arg = f"{name}=_{name}_default"
|
|
127
|
-
else:
|
|
128
|
-
arg = f"{name}: _{name}_type = _{name}_default"
|
|
129
|
-
globs[f"_{name}_default"] = attrib.default
|
|
130
|
-
elif attrib.default_factory is not NOTHING:
|
|
131
|
-
# Use NONE here and call the factory later
|
|
132
|
-
# This matches the behaviour of compiled
|
|
89
|
+
# Identify if method is static, if so include first arg, otherwise skip
|
|
90
|
+
is_static = type(cls.__dict__.get(extra_funcname)) is staticmethod
|
|
91
|
+
|
|
92
|
+
arglist = (
|
|
93
|
+
func_code.co_varnames[:argcount]
|
|
94
|
+
if is_static
|
|
95
|
+
else func_code.co_varnames[1:argcount]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
func_arglist.extend(arglist)
|
|
99
|
+
|
|
100
|
+
if extra_funcname == POST_INIT_FUNC:
|
|
101
|
+
post_init_annotations.update(func.__annotations__)
|
|
102
|
+
|
|
103
|
+
pos_arglist = []
|
|
104
|
+
kw_only_arglist = []
|
|
105
|
+
for name, attrib in attributes.items():
|
|
106
|
+
# post_init annotations can be used to broaden types.
|
|
107
|
+
if name in post_init_annotations:
|
|
108
|
+
globs[f"_{name}_type"] = post_init_annotations[name]
|
|
109
|
+
elif attrib.type is not NOTHING:
|
|
110
|
+
globs[f"_{name}_type"] = attrib.type
|
|
111
|
+
|
|
112
|
+
if attrib.init:
|
|
113
|
+
if attrib.default is not NOTHING:
|
|
114
|
+
if isinstance(attrib.default, (str, int, float, bool)):
|
|
115
|
+
# Just use the literal in these cases
|
|
133
116
|
if attrib.type is NOTHING:
|
|
134
|
-
arg = f"{name}=
|
|
117
|
+
arg = f"{name}={attrib.default!r}"
|
|
135
118
|
else:
|
|
136
|
-
arg = f"{name}: _{name}_type =
|
|
137
|
-
globs[f"_{name}_factory"] = attrib.default_factory
|
|
119
|
+
arg = f"{name}: _{name}_type = {attrib.default!r}"
|
|
138
120
|
else:
|
|
121
|
+
# No guarantee repr will work for other objects
|
|
122
|
+
# so store the value in a variable and put it
|
|
123
|
+
# in the globals dict for eval
|
|
139
124
|
if attrib.type is NOTHING:
|
|
140
|
-
arg = name
|
|
125
|
+
arg = f"{name}=_{name}_default"
|
|
141
126
|
else:
|
|
142
|
-
arg = f"{name}: _{name}_type"
|
|
143
|
-
if attrib.kw_only or kw_only:
|
|
144
|
-
kw_only_arglist.append(arg)
|
|
145
|
-
else:
|
|
146
|
-
pos_arglist.append(arg)
|
|
147
|
-
# Not in init, but need to set defaults
|
|
148
|
-
else:
|
|
149
|
-
if attrib.default is not NOTHING:
|
|
127
|
+
arg = f"{name}: _{name}_type = _{name}_default"
|
|
150
128
|
globs[f"_{name}_default"] = attrib.default
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if pos_args and kw_args:
|
|
157
|
-
args = f"{pos_args}, *, {kw_args}"
|
|
158
|
-
elif kw_args:
|
|
159
|
-
args = f"*, {kw_args}"
|
|
160
|
-
else:
|
|
161
|
-
args = pos_args
|
|
162
|
-
|
|
163
|
-
assignments = []
|
|
164
|
-
processes = [] # post_init values still need default factories to be called.
|
|
165
|
-
for name, attrib in attributes.items():
|
|
166
|
-
if attrib.init:
|
|
167
|
-
if attrib.default_factory is not NOTHING:
|
|
168
|
-
value = f"{name} if {name} is not None else _{name}_factory()"
|
|
129
|
+
elif attrib.default_factory is not NOTHING:
|
|
130
|
+
# Use NONE here and call the factory later
|
|
131
|
+
# This matches the behaviour of compiled
|
|
132
|
+
if attrib.type is NOTHING:
|
|
133
|
+
arg = f"{name}=None"
|
|
169
134
|
else:
|
|
170
|
-
|
|
135
|
+
arg = f"{name}: _{name}_type = None"
|
|
136
|
+
globs[f"_{name}_factory"] = attrib.default_factory
|
|
171
137
|
else:
|
|
172
|
-
if attrib.
|
|
173
|
-
|
|
174
|
-
elif attrib.default is not NOTHING:
|
|
175
|
-
value = f"_{name}_default"
|
|
138
|
+
if attrib.type is NOTHING:
|
|
139
|
+
arg = name
|
|
176
140
|
else:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
assignments.append((name, value))
|
|
184
|
-
|
|
185
|
-
if hasattr(cls, PRE_INIT_FUNC):
|
|
186
|
-
pre_init_arg_call = ", ".join(f"{name}={name}" for name in pre_init_args)
|
|
187
|
-
pre_init_call = f" self.{PRE_INIT_FUNC}({pre_init_arg_call})\n"
|
|
141
|
+
arg = f"{name}: _{name}_type"
|
|
142
|
+
if attrib.kw_only or kw_only:
|
|
143
|
+
kw_only_arglist.append(arg)
|
|
144
|
+
else:
|
|
145
|
+
pos_arglist.append(arg)
|
|
146
|
+
# Not in init, but need to set defaults
|
|
188
147
|
else:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
148
|
+
if attrib.default is not NOTHING:
|
|
149
|
+
globs[f"_{name}_default"] = attrib.default
|
|
150
|
+
elif attrib.default_factory is not NOTHING:
|
|
151
|
+
globs[f"_{name}_factory"] = attrib.default_factory
|
|
152
|
+
|
|
153
|
+
pos_args = ", ".join(pos_arglist)
|
|
154
|
+
kw_args = ", ".join(kw_only_arglist)
|
|
155
|
+
if pos_args and kw_args:
|
|
156
|
+
args = f"{pos_args}, *, {kw_args}"
|
|
157
|
+
elif kw_args:
|
|
158
|
+
args = f"*, {kw_args}"
|
|
159
|
+
else:
|
|
160
|
+
args = pos_args
|
|
161
|
+
|
|
162
|
+
assignments = []
|
|
163
|
+
processes = [] # post_init values still need default factories to be called.
|
|
164
|
+
for name, attrib in attributes.items():
|
|
165
|
+
if attrib.init:
|
|
166
|
+
if attrib.default_factory is not NOTHING:
|
|
167
|
+
value = f"{name} if {name} is not None else _{name}_factory()"
|
|
168
|
+
else:
|
|
169
|
+
value = name
|
|
198
170
|
else:
|
|
199
|
-
|
|
171
|
+
if attrib.default_factory is not NOTHING:
|
|
172
|
+
value = f"_{name}_factory()"
|
|
173
|
+
elif attrib.default is not NOTHING:
|
|
174
|
+
value = f"_{name}_default"
|
|
175
|
+
else:
|
|
176
|
+
value = None
|
|
200
177
|
|
|
201
|
-
if
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
178
|
+
if name in post_init_args:
|
|
179
|
+
if attrib.default_factory is not NOTHING:
|
|
180
|
+
processes.append((name, value))
|
|
181
|
+
elif value is not None:
|
|
182
|
+
assignments.append((name, value))
|
|
183
|
+
|
|
184
|
+
if hasattr(cls, PRE_INIT_FUNC):
|
|
185
|
+
pre_init_arg_call = ", ".join(f"{name}={name}" for name in pre_init_args)
|
|
186
|
+
pre_init_call = f" self.{PRE_INIT_FUNC}({pre_init_arg_call})\n"
|
|
187
|
+
else:
|
|
188
|
+
pre_init_call = ""
|
|
206
189
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
f"{
|
|
211
|
-
f"{post_init_call}\n"
|
|
190
|
+
if assignments or processes:
|
|
191
|
+
body = ""
|
|
192
|
+
body += "\n".join(
|
|
193
|
+
f" self.{name} = {value}" for name, value in assignments
|
|
212
194
|
)
|
|
213
|
-
|
|
195
|
+
body += "\n"
|
|
196
|
+
body += "\n".join(f" {name} = {value}" for name, value in processes)
|
|
197
|
+
else:
|
|
198
|
+
body = " pass"
|
|
214
199
|
|
|
215
|
-
|
|
200
|
+
if hasattr(cls, POST_INIT_FUNC):
|
|
201
|
+
post_init_arg_call = ", ".join(f"{name}={name}" for name in post_init_args)
|
|
202
|
+
post_init_call = f" self.{POST_INIT_FUNC}({post_init_arg_call})\n"
|
|
203
|
+
else:
|
|
204
|
+
post_init_call = ""
|
|
205
|
+
|
|
206
|
+
code = (
|
|
207
|
+
f"def {funcname}(self, {args}):\n"
|
|
208
|
+
f"{pre_init_call}\n"
|
|
209
|
+
f"{body}\n"
|
|
210
|
+
f"{post_init_call}\n"
|
|
211
|
+
)
|
|
216
212
|
|
|
213
|
+
return GeneratedCode(code, globs)
|
|
217
214
|
|
|
218
|
-
def get_iter_maker():
|
|
219
|
-
def __iter__(cls: type) -> GeneratedCode:
|
|
220
|
-
fields = get_attributes(cls)
|
|
221
215
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if attrib.iter
|
|
225
|
-
)
|
|
216
|
+
def iter_generator(cls, funcname="__iter__"):
|
|
217
|
+
fields = get_attributes(cls)
|
|
226
218
|
|
|
227
|
-
|
|
219
|
+
valid_fields = (
|
|
220
|
+
name for name, attrib in fields.items()
|
|
221
|
+
if attrib.iter
|
|
222
|
+
)
|
|
228
223
|
|
|
229
|
-
|
|
230
|
-
if not values:
|
|
231
|
-
values = " yield from ()"
|
|
224
|
+
values = "\n".join(f" yield self.{name}" for name in valid_fields)
|
|
232
225
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
226
|
+
# if values is an empty string
|
|
227
|
+
if not values:
|
|
228
|
+
values = " yield from ()"
|
|
236
229
|
|
|
237
|
-
|
|
230
|
+
code = f"def {funcname}(self):\n{values}"
|
|
231
|
+
globs = {}
|
|
232
|
+
return GeneratedCode(code, globs)
|
|
238
233
|
|
|
239
234
|
|
|
240
|
-
def
|
|
241
|
-
|
|
242
|
-
fields = get_attributes(cls)
|
|
235
|
+
def as_dict_generator(cls, funcname="as_dict"):
|
|
236
|
+
fields = get_attributes(cls)
|
|
243
237
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
238
|
+
vals = ", ".join(
|
|
239
|
+
f"'{name}': self.{name}"
|
|
240
|
+
for name, attrib in fields.items()
|
|
241
|
+
if attrib.serialize
|
|
242
|
+
)
|
|
243
|
+
out_dict = f"{{{vals}}}"
|
|
244
|
+
code = f"def {funcname}(self): return {out_dict}"
|
|
251
245
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return MethodMaker("as_dict", as_dict_gen)
|
|
246
|
+
globs = {}
|
|
247
|
+
return GeneratedCode(code, globs)
|
|
255
248
|
|
|
256
249
|
|
|
257
|
-
init_maker =
|
|
258
|
-
prefab_init_maker =
|
|
250
|
+
init_maker = MethodMaker("__init__", init_generator)
|
|
251
|
+
prefab_init_maker = MethodMaker(PREFAB_INIT_FUNC, init_generator)
|
|
259
252
|
repr_maker = MethodMaker(
|
|
260
253
|
"__repr__",
|
|
261
254
|
get_repr_generator(recursion_safe=False, eval_safe=True)
|
|
@@ -264,8 +257,8 @@ recursive_repr_maker = MethodMaker(
|
|
|
264
257
|
"__repr__",
|
|
265
258
|
get_repr_generator(recursion_safe=True, eval_safe=True)
|
|
266
259
|
)
|
|
267
|
-
iter_maker =
|
|
268
|
-
asdict_maker =
|
|
260
|
+
iter_maker = MethodMaker("__iter__", iter_generator)
|
|
261
|
+
asdict_maker = MethodMaker("as_dict", as_dict_generator)
|
|
269
262
|
|
|
270
263
|
|
|
271
264
|
# Updated field with additional attributes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/prefab.pyi
RENAMED
|
@@ -2,11 +2,14 @@ import typing
|
|
|
2
2
|
from types import MappingProxyType
|
|
3
3
|
from typing_extensions import dataclass_transform
|
|
4
4
|
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
5
7
|
from collections.abc import Callable
|
|
6
8
|
|
|
7
9
|
from . import (
|
|
8
10
|
NOTHING,
|
|
9
11
|
Field,
|
|
12
|
+
GeneratedCode,
|
|
10
13
|
MethodMaker,
|
|
11
14
|
SlotMakerMeta,
|
|
12
15
|
)
|
|
@@ -27,12 +30,9 @@ class PrefabError(Exception): ...
|
|
|
27
30
|
|
|
28
31
|
def get_attributes(cls: type) -> dict[str, Attribute]: ...
|
|
29
32
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
def get_asdict_maker() -> MethodMaker: ...
|
|
35
|
-
|
|
33
|
+
def init_generator(cls: type, funcname: str = "__init__") -> GeneratedCode: ...
|
|
34
|
+
def iter_generator(cls: type, funcname: str = "__iter__") -> GeneratedCode: ...
|
|
35
|
+
def as_dict_generator(cls: type, funcname: str = "as_dict") -> GeneratedCode: ...
|
|
36
36
|
|
|
37
37
|
init_maker: MethodMaker
|
|
38
38
|
prefab_init_maker: MethodMaker
|
|
@@ -44,6 +44,7 @@ asdict_maker: MethodMaker
|
|
|
44
44
|
|
|
45
45
|
class Attribute(Field):
|
|
46
46
|
__slots__: dict
|
|
47
|
+
__signature__: inspect.Signature
|
|
47
48
|
|
|
48
49
|
iter: bool
|
|
49
50
|
serialize: bool
|
|
@@ -8,6 +8,7 @@ from ducktools.classbuilder import (
|
|
|
8
8
|
|
|
9
9
|
builder,
|
|
10
10
|
default_methods,
|
|
11
|
+
eq_maker,
|
|
11
12
|
get_fields,
|
|
12
13
|
get_flags,
|
|
13
14
|
get_methods,
|
|
@@ -50,8 +51,8 @@ def test_get_fields_flags_methods():
|
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
def test_method_maker():
|
|
53
|
-
def generator(cls):
|
|
54
|
-
code = "def
|
|
54
|
+
def generator(cls, funcname="demo"):
|
|
55
|
+
code = f"def {funcname}(self): return self.x"
|
|
55
56
|
globs = {}
|
|
56
57
|
return GeneratedCode(code, globs)
|
|
57
58
|
|
|
@@ -527,3 +528,33 @@ def test_signature():
|
|
|
527
528
|
__slots__ = SlotFields(x=42)
|
|
528
529
|
|
|
529
530
|
assert str(inspect.signature(SigClass)) == "(x=42)"
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def test_subclass_method_not_overwritten():
|
|
534
|
+
@slotclass
|
|
535
|
+
class X:
|
|
536
|
+
__slots__ = SlotFields(x=Field())
|
|
537
|
+
|
|
538
|
+
class Y(X):
|
|
539
|
+
def __init__(self, x, y):
|
|
540
|
+
self.y = y
|
|
541
|
+
super().__init__(x=x)
|
|
542
|
+
|
|
543
|
+
y_init_func = Y.__init__
|
|
544
|
+
|
|
545
|
+
assert X.__dict__["__eq__"] is eq_maker
|
|
546
|
+
|
|
547
|
+
y_inst = Y(0, 1)
|
|
548
|
+
|
|
549
|
+
# super().__init__ method generated correctly
|
|
550
|
+
assert y_init_func is Y.__init__
|
|
551
|
+
assert X.__dict__["__init__"] is not init_maker
|
|
552
|
+
assert (y_inst.x, y_inst.y) == (0, 1)
|
|
553
|
+
|
|
554
|
+
# Would fail previously as __init__ would be overwritten
|
|
555
|
+
y_inst_2 = Y(0, 2)
|
|
556
|
+
|
|
557
|
+
assert y_inst == y_inst_2
|
|
558
|
+
|
|
559
|
+
assert X.__dict__["__eq__"] is not eq_maker
|
|
560
|
+
assert "__eq__" not in Y.__dict__
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Approaches and tools #
|
|
2
|
-
|
|
3
|
-
As this module's code generation is inspired by the workings of [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
|
|
4
|
-
I thought it was briefly worth discussing his note on learning an approach vs using a tool.
|
|
5
|
-
|
|
6
|
-
I think that learning an approach is valuable, this module would not exist without the
|
|
7
|
-
example given by `cluegen`. It also wouldn't exist if I hadn't needed to extend `cluegen`
|
|
8
|
-
for some basic features (try using `Path` default values with `cluegen`).
|
|
9
|
-
|
|
10
|
-
In the general spirit though, this module intends to provide some basic tools to help
|
|
11
|
-
build your own custom class generators.
|
|
12
|
-
The generator included in the base module is intended to be used to help 'bootstrap' a
|
|
13
|
-
modified generator with features that work how **you** want them to work.
|
|
14
|
-
|
|
15
|
-
The `prefab` module is the more fully featured powertool *I* built with these tools.
|
|
16
|
-
However, much like a prefabricated building, it may not be what you desire.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/docs/perf/performance_tests.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/src/ducktools/classbuilder/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/annotations/test_annotated.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/dynamic/test_internals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_creation.py
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_dunders.py
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_frozen.py
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_funcs.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_init.py
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_kw_only.py
RENAMED
|
File without changes
|
{ducktools_classbuilder-0.6.0 → ducktools_classbuilder-0.6.2}/tests/prefab/shared/test_repr.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|