ducktools-classbuilder 0.7.0__tar.gz → 0.7.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.7.0 → ducktools_classbuilder-0.7.2}/PKG-INFO +2 -1
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/extension_examples.md +134 -2
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/tutorial.md +10 -8
- ducktools_classbuilder-0.7.2/docs_code/docs_ex10_frozen_attributes.py +125 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex3_iterable.py +1 -1
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/tutorial_code.py +4 -2
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/pyproject.toml +1 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/__init__.py +14 -17
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/__init__.pyi +7 -7
- ducktools_classbuilder-0.7.2/src/ducktools/classbuilder/_version.py +2 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/annotations.py +106 -23
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/annotations.pyi +6 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/PKG-INFO +2 -1
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/SOURCES.txt +4 -1
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/annotations/test_annotated.py +12 -7
- ducktools_classbuilder-0.7.2/tests/annotations/test_future_annotations.py +45 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/conftest.py +2 -2
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_slotted_class.py +0 -1
- ducktools_classbuilder-0.7.2/tests/py314_tests/test_forwardref_annotations.py +45 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/test_core.py +27 -1
- ducktools_classbuilder-0.7.0/src/ducktools/classbuilder/_version.py +0 -2
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.github/dependabot.yml +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.github/workflows/auto_test.yml +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.github/workflows/publish_to_pypi.yml +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.github/workflows/publish_to_testpypi.yml +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.gitignore +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/.readthedocs.yaml +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/LICENSE.md +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/MANIFEST.in +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/README.md +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/Makefile +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/api.md +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/approach_vs_tool.md +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/conf.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/generated_code.md +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/index.md +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/make.bat +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/perf/performance_tests.md +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs/prefab/index.md +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex1_basic.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex2_register.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex5_frozen.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex7_posonly.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex8_converters.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex9_annotated.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/perf/cluegen.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/perf/dataklasses.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/perf/hyperfine_testmaker.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/perf/perf_profile.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/setup.cfg +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/prefab.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/prefab.pyi +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/py.typed +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/dependency_links.txt +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/requires.txt +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools_classbuilder.egg-info/top_level.txt +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/annotations/test_annotations_module.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_compare_attrib.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_construction.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_frozen.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_internals.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_pre_post_init.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_private.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_slots_novalues.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/dynamic/test_subclass_implementation.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/conftest.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/creation.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/creation_empty.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/dunders.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/creation_1.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/creation_2.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/creation_3.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/creation_5.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/inheritance_1.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/fails/inheritance_2.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/funcs_prefabs.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/hint_syntax.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/inheritance.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/init_ex.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/kw_only.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/examples/repr_func.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_creation.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_dunders.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_funcs.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_hint_syntax.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_inheritance.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_init.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_kw_only.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/prefab/shared/test_repr.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/py312_tests/test_generic_annotations.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/test_field_flags.py +0 -0
- {ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/tests/test_slotmakermeta.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ducktools-classbuilder
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Toolkit for creating class boilerplate generators
|
|
5
5
|
Author: David C Ellis
|
|
6
6
|
License: MIT License
|
|
@@ -32,6 +32,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
32
32
|
Classifier: Programming Language :: Python :: 3.10
|
|
33
33
|
Classifier: Programming Language :: Python :: 3.11
|
|
34
34
|
Classifier: Programming Language :: Python :: 3.12
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
35
36
|
Classifier: Operating System :: OS Independent
|
|
36
37
|
Classifier: License :: OSI Approved :: MIT License
|
|
37
38
|
Requires-Python: >=3.8
|
|
@@ -84,7 +84,7 @@ def iter_generator(cls, funcname="__iter__"):
|
|
|
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 {funcname}(self):\n
|
|
87
|
+
code = f"def {funcname}(self):\n{field_yield}"
|
|
88
88
|
globs = {}
|
|
89
89
|
return GeneratedCode(code, globs)
|
|
90
90
|
|
|
@@ -259,6 +259,138 @@ if __name__ == "__main__":
|
|
|
259
259
|
print(e)
|
|
260
260
|
```
|
|
261
261
|
|
|
262
|
+
#### Frozen Attributes ####
|
|
263
|
+
|
|
264
|
+
Here's an implementation that allows freezing of individual attributes.
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
import ducktools.classbuilder as dtbuild
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class FreezableField(dtbuild.Field):
|
|
271
|
+
frozen: bool = False
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def setattr_generator(cls, funcname="__setattr__"):
|
|
275
|
+
globs = {}
|
|
276
|
+
|
|
277
|
+
flags = dtbuild.get_flags(cls)
|
|
278
|
+
fields = dtbuild.get_fields(cls)
|
|
279
|
+
|
|
280
|
+
frozen_fields = set(
|
|
281
|
+
name for name, field in fields.items()
|
|
282
|
+
if getattr(field, "frozen", False)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
globs["__frozen_fields"] = frozen_fields
|
|
286
|
+
|
|
287
|
+
if flags.get("slotted", True):
|
|
288
|
+
globs["__setattr_func"] = object.__setattr__
|
|
289
|
+
setattr_method = "__setattr_func(self, name, value)"
|
|
290
|
+
attrib_check = "hasattr(self, name)"
|
|
291
|
+
else:
|
|
292
|
+
setattr_method = "self.__dict__[name] = value"
|
|
293
|
+
attrib_check = "name in self.__dict__"
|
|
294
|
+
|
|
295
|
+
code = (
|
|
296
|
+
f"def {funcname}(self, name, value):\n"
|
|
297
|
+
f" if name in __frozen_fields and {attrib_check}:\n"
|
|
298
|
+
f" raise AttributeError(\n"
|
|
299
|
+
f" f'Attribute {{name!r}} does not support assignment'\n"
|
|
300
|
+
f" )\n"
|
|
301
|
+
f" else:\n"
|
|
302
|
+
f" {setattr_method}\n"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return dtbuild.GeneratedCode(code, globs)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def delattr_generator(cls, funcname="__delattr__"):
|
|
309
|
+
globs = {}
|
|
310
|
+
|
|
311
|
+
flags = dtbuild.get_flags(cls)
|
|
312
|
+
fields = dtbuild.get_fields(cls)
|
|
313
|
+
|
|
314
|
+
frozen_fields = set(
|
|
315
|
+
name for name, field in fields.items()
|
|
316
|
+
if getattr(field, "frozen", False)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
globs["__frozen_fields"] = frozen_fields
|
|
320
|
+
|
|
321
|
+
if flags.get("slotted", True):
|
|
322
|
+
globs["__delattr_func"] = object.__delattr__
|
|
323
|
+
delattr_method = "__delattr_func(self, name)"
|
|
324
|
+
else:
|
|
325
|
+
delattr_method = "del self.__dict__[name]"
|
|
326
|
+
|
|
327
|
+
code = (
|
|
328
|
+
f"def {funcname}(self, name):\n"
|
|
329
|
+
f" if name in __frozen_fields:"
|
|
330
|
+
f" raise AttributeError(\n"
|
|
331
|
+
f" f'Attribute {{name!r}} is frozen and can not be deleted'\n"
|
|
332
|
+
f" )\n"
|
|
333
|
+
f" else:\n"
|
|
334
|
+
f" {delattr_method}\n"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return dtbuild.GeneratedCode(code, globs)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
frozen_setattr_field_maker = dtbuild.MethodMaker("__setattr__", setattr_generator)
|
|
341
|
+
frozen_delattr_field_maker = dtbuild.MethodMaker("__delattr__", delattr_generator)
|
|
342
|
+
gatherer = dtbuild.make_unified_gatherer(FreezableField)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def freezable(cls=None, /, *, frozen=False):
|
|
346
|
+
if cls is None:
|
|
347
|
+
return lambda cls_: freezable(cls_, frozen=frozen)
|
|
348
|
+
|
|
349
|
+
# To make a slotted class use a base class with metaclass
|
|
350
|
+
flags = {"frozen": frozen, "slotted": False}
|
|
351
|
+
|
|
352
|
+
cls = dtbuild.builder(
|
|
353
|
+
cls,
|
|
354
|
+
gatherer=gatherer,
|
|
355
|
+
methods=dtbuild.default_methods,
|
|
356
|
+
flags=flags,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Frozen attributes need to be added afterwards
|
|
360
|
+
# Due to the need to know if frozen fields exist
|
|
361
|
+
if frozen:
|
|
362
|
+
setattr(cls, "__setattr__", dtbuild.frozen_setattr_maker)
|
|
363
|
+
setattr(cls, "__delattr__", dtbuild.frozen_delattr_maker)
|
|
364
|
+
else:
|
|
365
|
+
fields = dtbuild.get_fields(cls)
|
|
366
|
+
has_frozen_fields = False
|
|
367
|
+
for f in fields.values():
|
|
368
|
+
if getattr(f, "frozen", False):
|
|
369
|
+
has_frozen_fields = True
|
|
370
|
+
break
|
|
371
|
+
|
|
372
|
+
if has_frozen_fields:
|
|
373
|
+
setattr(cls, "__setattr__", frozen_setattr_field_maker)
|
|
374
|
+
setattr(cls, "__delattr__", frozen_delattr_field_maker)
|
|
375
|
+
|
|
376
|
+
return cls
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@freezable
|
|
380
|
+
class X:
|
|
381
|
+
a: int = 2
|
|
382
|
+
b: int = FreezableField(default=12, frozen=True)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
x = X()
|
|
386
|
+
x.a = 21
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
x.b = 43
|
|
390
|
+
except AttributeError as e:
|
|
391
|
+
print(repr(e))
|
|
392
|
+
```
|
|
393
|
+
|
|
262
394
|
#### Converters ####
|
|
263
395
|
|
|
264
396
|
Here's an implementation of basic converters that always convert when
|
|
@@ -330,7 +462,7 @@ if __name__ == "__main__":
|
|
|
330
462
|
|
|
331
463
|
This seems to be a feature people keep requesting for `dataclasses`.
|
|
332
464
|
|
|
333
|
-
To implement this you
|
|
465
|
+
To implement this you need to create a new annotated_gatherer function.
|
|
334
466
|
|
|
335
467
|
> Note: Field classes will be frozen when running under pytest.
|
|
336
468
|
> They should not be mutated by gatherers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tutorial: Making a class boilerplate generator #
|
|
2
2
|
|
|
3
|
-
The core idea is that there are
|
|
3
|
+
The core idea is that there are 4 parts to the process of generating
|
|
4
4
|
the class boilerplate that need to be handled:
|
|
5
5
|
|
|
6
6
|
1. Create a new subclass of `Field` if you need to add any extra attributes to fields
|
|
@@ -162,7 +162,7 @@ print(report_generator(CodegenDemo).source_code)
|
|
|
162
162
|
Here we will make both a simple decorator based builder and then a subclass
|
|
163
163
|
based builder that can create `__slots__`.
|
|
164
164
|
|
|
165
|
-
### Decorator builder ###
|
|
165
|
+
### 4a: Decorator builder ###
|
|
166
166
|
```python
|
|
167
167
|
def reportclass(cls):
|
|
168
168
|
gatherer = fields_attribute_gatherer
|
|
@@ -177,21 +177,22 @@ def reportclass(cls):
|
|
|
177
177
|
flags = {"slotted": slotted}
|
|
178
178
|
|
|
179
179
|
return dtbuild.builder(cls, gatherer=gatherer, methods=methods, flags=flags)
|
|
180
|
+
```
|
|
180
181
|
|
|
181
|
-
|
|
182
|
+
### 4b: Base class Builder ###
|
|
183
|
+
```python
|
|
182
184
|
# Once slots have been made, slot_gatherer should be used.
|
|
183
185
|
slot_gatherer = dtbuild.make_slot_gatherer(CustomField)
|
|
184
|
-
```
|
|
185
186
|
|
|
186
|
-
|
|
187
|
-
```python
|
|
187
|
+
|
|
188
188
|
class ReportClass(metaclass=dtbuild.SlotMakerMeta):
|
|
189
189
|
__slots__ = {}
|
|
190
190
|
_meta_gatherer = fields_attribute_gatherer
|
|
191
191
|
|
|
192
192
|
def __init_subclass__(cls):
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
# Check if the metaclass has generated slots
|
|
194
|
+
meta_slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
|
|
195
|
+
gatherer = slot_gatherer if meta_slotted else fields_attribute_gatherer
|
|
195
196
|
methods = {
|
|
196
197
|
dtbuild.eq_maker,
|
|
197
198
|
dtbuild.repr_maker,
|
|
@@ -199,6 +200,7 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
|
|
|
199
200
|
report_maker
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
# The class may still have slots unrelated to code generation
|
|
202
204
|
slotted = "__slots__" in vars(cls)
|
|
203
205
|
flags = {"slotted": slotted}
|
|
204
206
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import ducktools.classbuilder as dtbuild
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FreezableField(dtbuild.Field):
|
|
5
|
+
frozen: bool = False
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setattr_generator(cls, funcname="__setattr__"):
|
|
9
|
+
globs = {}
|
|
10
|
+
|
|
11
|
+
flags = dtbuild.get_flags(cls)
|
|
12
|
+
fields = dtbuild.get_fields(cls)
|
|
13
|
+
|
|
14
|
+
frozen_fields = set(
|
|
15
|
+
name for name, field in fields.items()
|
|
16
|
+
if getattr(field, "frozen", False)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
globs["__frozen_fields"] = frozen_fields
|
|
20
|
+
|
|
21
|
+
if flags.get("slotted", True):
|
|
22
|
+
globs["__setattr_func"] = object.__setattr__
|
|
23
|
+
setattr_method = "__setattr_func(self, name, value)"
|
|
24
|
+
attrib_check = "hasattr(self, name)"
|
|
25
|
+
else:
|
|
26
|
+
setattr_method = "self.__dict__[name] = value"
|
|
27
|
+
attrib_check = "name in self.__dict__"
|
|
28
|
+
|
|
29
|
+
code = (
|
|
30
|
+
f"def {funcname}(self, name, value):\n"
|
|
31
|
+
f" if name in __frozen_fields and {attrib_check}:\n"
|
|
32
|
+
f" raise AttributeError(\n"
|
|
33
|
+
f" f'Attribute {{name!r}} does not support assignment'\n"
|
|
34
|
+
f" )\n"
|
|
35
|
+
f" else:\n"
|
|
36
|
+
f" {setattr_method}\n"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return dtbuild.GeneratedCode(code, globs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def delattr_generator(cls, funcname="__delattr__"):
|
|
43
|
+
globs = {}
|
|
44
|
+
|
|
45
|
+
flags = dtbuild.get_flags(cls)
|
|
46
|
+
fields = dtbuild.get_fields(cls)
|
|
47
|
+
|
|
48
|
+
frozen_fields = set(
|
|
49
|
+
name for name, field in fields.items()
|
|
50
|
+
if getattr(field, "frozen", False)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
globs["__frozen_fields"] = frozen_fields
|
|
54
|
+
|
|
55
|
+
if flags.get("slotted", True):
|
|
56
|
+
globs["__delattr_func"] = object.__delattr__
|
|
57
|
+
delattr_method = "__delattr_func(self, name)"
|
|
58
|
+
else:
|
|
59
|
+
delattr_method = "del self.__dict__[name]"
|
|
60
|
+
|
|
61
|
+
code = (
|
|
62
|
+
f"def {funcname}(self, name):\n"
|
|
63
|
+
f" if name in __frozen_fields:"
|
|
64
|
+
f" raise AttributeError(\n"
|
|
65
|
+
f" f'Attribute {{name!r}} is frozen and can not be deleted'\n"
|
|
66
|
+
f" )\n"
|
|
67
|
+
f" else:\n"
|
|
68
|
+
f" {delattr_method}\n"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return dtbuild.GeneratedCode(code, globs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
frozen_setattr_field_maker = dtbuild.MethodMaker("__setattr__", setattr_generator)
|
|
75
|
+
frozen_delattr_field_maker = dtbuild.MethodMaker("__delattr__", delattr_generator)
|
|
76
|
+
gatherer = dtbuild.make_unified_gatherer(FreezableField)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def freezable(cls=None, /, *, frozen=False):
|
|
80
|
+
if cls is None:
|
|
81
|
+
return lambda cls_: freezable(cls_, frozen=frozen)
|
|
82
|
+
|
|
83
|
+
# To make a slotted class use a base class with metaclass
|
|
84
|
+
flags = {"frozen": frozen, "slotted": False}
|
|
85
|
+
|
|
86
|
+
cls = dtbuild.builder(
|
|
87
|
+
cls,
|
|
88
|
+
gatherer=gatherer,
|
|
89
|
+
methods=dtbuild.default_methods,
|
|
90
|
+
flags=flags,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Frozen attributes need to be added afterwards
|
|
94
|
+
# Due to the need to know if frozen fields exist
|
|
95
|
+
if frozen:
|
|
96
|
+
setattr(cls, "__setattr__", dtbuild.frozen_setattr_maker)
|
|
97
|
+
setattr(cls, "__delattr__", dtbuild.frozen_delattr_maker)
|
|
98
|
+
else:
|
|
99
|
+
fields = dtbuild.get_fields(cls)
|
|
100
|
+
has_frozen_fields = False
|
|
101
|
+
for f in fields.values():
|
|
102
|
+
if getattr(f, "frozen", False):
|
|
103
|
+
has_frozen_fields = True
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if has_frozen_fields:
|
|
107
|
+
setattr(cls, "__setattr__", frozen_setattr_field_maker)
|
|
108
|
+
setattr(cls, "__delattr__", frozen_delattr_field_maker)
|
|
109
|
+
|
|
110
|
+
return cls
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@freezable
|
|
114
|
+
class X:
|
|
115
|
+
a: int = 2
|
|
116
|
+
b: int = FreezableField(default=12, frozen=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
x = X()
|
|
120
|
+
x.a = 21
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
x.b = 43
|
|
124
|
+
except AttributeError as e:
|
|
125
|
+
print(repr(e))
|
{ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/docs_code/docs_ex3_iterable.py
RENAMED
|
@@ -13,7 +13,7 @@ def iter_generator(cls, funcname="__iter__"):
|
|
|
13
13
|
field_yield = "\n".join(f" yield self.{f}" for f in field_names)
|
|
14
14
|
if not field_yield:
|
|
15
15
|
field_yield = " yield from ()"
|
|
16
|
-
code = f"def {funcname}(self):\n
|
|
16
|
+
code = f"def {funcname}(self):\n{field_yield}"
|
|
17
17
|
globs = {}
|
|
18
18
|
return GeneratedCode(code, globs)
|
|
19
19
|
|
|
@@ -117,8 +117,9 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
|
|
|
117
117
|
_meta_gatherer = fields_attribute_gatherer
|
|
118
118
|
|
|
119
119
|
def __init_subclass__(cls):
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
# Check if the metaclass has generated slots
|
|
121
|
+
meta_slotted = '__slots__' in vars(cls) and isinstance(cls.__slots__, dtbuild.SlotFields)
|
|
122
|
+
gatherer = slot_gatherer if meta_slotted else fields_attribute_gatherer
|
|
122
123
|
methods = {
|
|
123
124
|
dtbuild.eq_maker,
|
|
124
125
|
dtbuild.repr_maker,
|
|
@@ -126,6 +127,7 @@ class ReportClass(metaclass=dtbuild.SlotMakerMeta):
|
|
|
126
127
|
report_maker
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
# The class may still have slots unrelated to code generation
|
|
129
131
|
slotted = "__slots__" in vars(cls)
|
|
130
132
|
flags = {"slotted": slotted}
|
|
131
133
|
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.10",
|
|
21
21
|
"Programming Language :: Python :: 3.11",
|
|
22
22
|
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
23
24
|
"Operating System :: OS Independent",
|
|
24
25
|
"License :: OSI Approved :: MIT License",
|
|
25
26
|
]
|
{ducktools_classbuilder-0.7.0 → ducktools_classbuilder-0.7.2}/src/ducktools/classbuilder/__init__.py
RENAMED
|
@@ -363,15 +363,17 @@ def eq_generator(cls, funcname="__eq__"):
|
|
|
363
363
|
]
|
|
364
364
|
|
|
365
365
|
if field_names:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
366
|
+
instance_comparison = "\n and ".join(
|
|
367
|
+
f"self.{name} == other.{name}" for name in field_names
|
|
368
|
+
)
|
|
369
369
|
else:
|
|
370
370
|
instance_comparison = "True"
|
|
371
371
|
|
|
372
372
|
code = (
|
|
373
373
|
f"def {funcname}(self, other):\n"
|
|
374
|
-
f" return
|
|
374
|
+
f" return (\n"
|
|
375
|
+
f" {instance_comparison}\n"
|
|
376
|
+
f" ) if {class_comparison} else NotImplemented\n"
|
|
375
377
|
)
|
|
376
378
|
globs = {}
|
|
377
379
|
|
|
@@ -390,11 +392,13 @@ def frozen_setattr_generator(cls, funcname="__setattr__"):
|
|
|
390
392
|
if flags.get("slotted", True):
|
|
391
393
|
globs["__setattr_func"] = object.__setattr__
|
|
392
394
|
setattr_method = "__setattr_func(self, name, value)"
|
|
395
|
+
hasattr_check = "hasattr(self, name)"
|
|
393
396
|
else:
|
|
394
397
|
setattr_method = "self.__dict__[name] = value"
|
|
398
|
+
hasattr_check = "name in self.__dict__"
|
|
395
399
|
|
|
396
400
|
body = (
|
|
397
|
-
f" if
|
|
401
|
+
f" if {hasattr_check} or name not in __field_names:\n"
|
|
398
402
|
f' raise TypeError(\n'
|
|
399
403
|
f' f"{{type(self).__name__!r}} object does not support "'
|
|
400
404
|
f' f"attribute assignment"\n'
|
|
@@ -707,10 +711,6 @@ def make_slot_gatherer(field_type=Field):
|
|
|
707
711
|
"in order to generate a slotclass"
|
|
708
712
|
)
|
|
709
713
|
|
|
710
|
-
# Don't want to mutate original annotations so make a copy if it exists
|
|
711
|
-
# Looking at the dict is a Python3.9 or earlier requirement
|
|
712
|
-
cls_annotations = get_ns_annotations(cls_dict)
|
|
713
|
-
|
|
714
714
|
cls_fields = {}
|
|
715
715
|
slot_replacement = {}
|
|
716
716
|
|
|
@@ -724,8 +724,6 @@ def make_slot_gatherer(field_type=Field):
|
|
|
724
724
|
|
|
725
725
|
if isinstance(v, field_type):
|
|
726
726
|
attrib = v
|
|
727
|
-
if attrib.type is not NOTHING:
|
|
728
|
-
cls_annotations[k] = attrib.type
|
|
729
727
|
else:
|
|
730
728
|
# Plain values treated as defaults
|
|
731
729
|
attrib = field_type(default=v)
|
|
@@ -738,7 +736,6 @@ def make_slot_gatherer(field_type=Field):
|
|
|
738
736
|
# In this case, slots with documentation and new annotations.
|
|
739
737
|
modifications = {
|
|
740
738
|
"__slots__": slot_replacement,
|
|
741
|
-
"__annotations__": cls_annotations,
|
|
742
739
|
}
|
|
743
740
|
|
|
744
741
|
return cls_fields, modifications
|
|
@@ -748,7 +745,7 @@ def make_slot_gatherer(field_type=Field):
|
|
|
748
745
|
|
|
749
746
|
def make_annotation_gatherer(
|
|
750
747
|
field_type=Field,
|
|
751
|
-
leave_default_values=
|
|
748
|
+
leave_default_values=False,
|
|
752
749
|
):
|
|
753
750
|
"""
|
|
754
751
|
Create a new annotation gatherer that will work with `Field` instances
|
|
@@ -814,7 +811,7 @@ def make_annotation_gatherer(
|
|
|
814
811
|
|
|
815
812
|
def make_field_gatherer(
|
|
816
813
|
field_type=Field,
|
|
817
|
-
leave_default_values=
|
|
814
|
+
leave_default_values=False,
|
|
818
815
|
):
|
|
819
816
|
def field_attribute_gatherer(cls_or_ns):
|
|
820
817
|
if isinstance(cls_or_ns, (_MappingProxyType, dict)):
|
|
@@ -847,7 +844,7 @@ def make_field_gatherer(
|
|
|
847
844
|
|
|
848
845
|
def make_unified_gatherer(
|
|
849
846
|
field_type=Field,
|
|
850
|
-
leave_default_values=
|
|
847
|
+
leave_default_values=False,
|
|
851
848
|
):
|
|
852
849
|
"""
|
|
853
850
|
Create a gatherer that will work via first slots, then
|
|
@@ -897,7 +894,7 @@ annotation_gatherer = make_annotation_gatherer()
|
|
|
897
894
|
|
|
898
895
|
# The unified gatherer used for slot classes must remove default
|
|
899
896
|
# values for slots to work correctly.
|
|
900
|
-
unified_gatherer = make_unified_gatherer(
|
|
897
|
+
unified_gatherer = make_unified_gatherer()
|
|
901
898
|
|
|
902
899
|
|
|
903
900
|
# Now the gatherers have been defined, add __repr__ and __eq__ to Field.
|
|
@@ -963,7 +960,7 @@ class AnnotationClass(metaclass=SlotMakerMeta):
|
|
|
963
960
|
def __init_subclass__(
|
|
964
961
|
cls,
|
|
965
962
|
methods=default_methods,
|
|
966
|
-
gatherer=
|
|
963
|
+
gatherer=unified_gatherer,
|
|
967
964
|
**kwargs
|
|
968
965
|
):
|
|
969
966
|
# Check class dict otherwise this will always be True as this base
|
|
@@ -178,37 +178,37 @@ def make_slot_gatherer(
|
|
|
178
178
|
@typing.overload
|
|
179
179
|
def make_annotation_gatherer(
|
|
180
180
|
field_type: type[_FieldType],
|
|
181
|
-
leave_default_values: bool =
|
|
181
|
+
leave_default_values: bool = False,
|
|
182
182
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
|
|
183
183
|
|
|
184
184
|
@typing.overload
|
|
185
185
|
def make_annotation_gatherer(
|
|
186
186
|
field_type: _ReturnsField = Field,
|
|
187
|
-
leave_default_values: bool =
|
|
187
|
+
leave_default_values: bool = False,
|
|
188
188
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
|
|
189
189
|
|
|
190
190
|
@typing.overload
|
|
191
191
|
def make_field_gatherer(
|
|
192
192
|
field_type: type[_FieldType],
|
|
193
|
-
leave_default_values: bool =
|
|
193
|
+
leave_default_values: bool = False,
|
|
194
194
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
|
|
195
195
|
|
|
196
196
|
@typing.overload
|
|
197
197
|
def make_field_gatherer(
|
|
198
198
|
field_type: _ReturnsField = Field,
|
|
199
|
-
leave_default_values: bool =
|
|
199
|
+
leave_default_values: bool = False,
|
|
200
200
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
|
|
201
201
|
|
|
202
202
|
@typing.overload
|
|
203
203
|
def make_unified_gatherer(
|
|
204
204
|
field_type: type[_FieldType],
|
|
205
|
-
leave_default_values: bool =
|
|
205
|
+
leave_default_values: bool = False,
|
|
206
206
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, _FieldType], dict[str, typing.Any]]]: ...
|
|
207
207
|
|
|
208
208
|
@typing.overload
|
|
209
209
|
def make_unified_gatherer(
|
|
210
210
|
field_type: _ReturnsField = Field,
|
|
211
|
-
leave_default_values: bool =
|
|
211
|
+
leave_default_values: bool = False,
|
|
212
212
|
) -> Callable[[type | _CopiableMappings], tuple[dict[str, Field], dict[str, typing.Any]]]: ...
|
|
213
213
|
|
|
214
214
|
|
|
@@ -249,7 +249,7 @@ class AnnotationClass(metaclass=SlotMakerMeta):
|
|
|
249
249
|
def __init_subclass__(
|
|
250
250
|
cls,
|
|
251
251
|
methods: frozenset[MethodMaker] | set[MethodMaker] = default_methods,
|
|
252
|
-
gatherer: _gatherer_type =
|
|
252
|
+
gatherer: _gatherer_type = unified_gatherer,
|
|
253
253
|
**kwargs,
|
|
254
254
|
) -> None: ...
|
|
255
255
|
|