Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/davidcellis/ducktools-classbuilder
Toolkit for creating Python class boilerplate generators
https://github.com/davidcellis/ducktools-classbuilder
Last synced: 1 day ago
JSON representation
Toolkit for creating Python class boilerplate generators
- Host: GitHub
- URL: https://github.com/davidcellis/ducktools-classbuilder
- Owner: DavidCEllis
- License: mit
- Created: 2024-04-08T13:08:24.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2024-08-06T12:28:19.000Z (3 months ago)
- Last Synced: 2024-08-21T09:49:30.368Z (3 months ago)
- Language: Python
- Homepage: https://ducktools-classbuilder.readthedocs.io
- Size: 336 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README
# Ducktools: Class Builder #
`ducktools-classbuilder` is *the* Python package that will bring you the **joy**
of writing... functions... that will bring back the **joy** of writing classes.Maybe.
While `attrs` and `dataclasses` are class boilerplate generators,
`ducktools.classbuilder` is intended to provide the tools to help make a customized
version of the same concept.Install from PyPI with:
`python -m pip install ducktools-classbuilder`## Included Implementations ##
There are 2 different implementations provided with the module each of which offers
a subclass based and decorator based option.> [!TIP]
> For more information on using these tools to create your own implementations
> using the builder see
> [the tutorial](https://ducktools-classbuilder.readthedocs.io/en/latest/tutorial.html)
> for a full tutorial and
> [extension_examples](https://ducktools-classbuilder.readthedocs.io/en/latest/extension_examples.html)
> for other customizations.### Core ###
These tools are available from the main `ducktools.classbuilder` module.
* `@slotclass`
* A decorator based implementation that uses a special dict subclass assigned
to `__slots__` to describe the fields for method generation.
* `AnnotationClass`
* A subclass based implementation that works with `__slots__`, type annotations
or `Field(...)` attributes to describe the fields for method generation.
* If `__slots__` isn't used to declare fields, it will be generated by a metaclass.Each of these forms of class generation will result in the same methods being
attached to the class after the field information has been obtained.```python
from ducktools.classbuilder import Field, SlotFields, slotclass@slotclass
class SlottedDC:
__slots__ = SlotFields(
the_answer=42,
the_question=Field(
default="What do you get if you multiply six by nine?",
doc="Life, the Universe, and Everything",
),
)
ex = SlottedDC()
print(ex)
```### Prefab ###
This is available from the `ducktools.classbuilder.prefab` submodule.
This includes more customization including `__prefab_pre_init__` and `__prefab_post_init__`
functions for subclass customization.A `@prefab` decorator and `Prefab` base class are provided.
Similar to `AnnotationClass`, `Prefab` will generate `__slots__` by default.
However decorated classes with `@prefab` that do not declare fields using `__slots__`
will **not** be slotted and there is no `slots` argument to apply this.Here is an example of applying a conversion in `__post_init__`:
```python
from pathlib import Path
from ducktools.classbuilder.prefab import Prefabclass AppDetails(Prefab, frozen=True):
app_name: str
app_path: Pathdef __prefab_post_init__(self, app_path: str | Path):
# frozen in `Prefab` is implemented as a 'set-once' __setattr__ function.
# So we do not need to use `object.__setattr__` here
self.app_path = Path(app_path)steam = AppDetails(
"Steam",
r"C:\Program Files (x86)\Steam\steam.exe"
)print(steam)
```## What is the issue with generating `__slots__` with a decorator ##
If you want to use `__slots__` in order to save memory you have to declare
them when the class is originally created as you can't add them later.When you use `@dataclass(slots=True)`[^2] with `dataclasses`, the function
has to make a new class and attempt to copy over everything from the original.This is because decorators operate on classes *after they have been created*
while slots need to be declared beforehand.
While you can change the value of `__slots__` after a class has been created,
this will have no effect on the internal structure of the class.By using a metaclass or by declaring fields using `__slots__` however,
the fields can be set *before* the class is constructed, so the class
will work correctly without needing to be rebuilt.For example these two classes would be roughly equivalent, except that
`@dataclass` has had to recreate the class from scratch while `AnnotationClass`
has created `__slots__` and added the methods on to the original class.
This means that any references stored to the original class *before*
`@dataclass` has rebuilt the class will not be pointing towards the
correct class.Here's a demonstration of the issue using a registry for serialization
functions.> This example requires Python 3.10 or later as earlier versions of
> `dataclasses` did not support the `slots` argument.```python
import json
from dataclasses import dataclass
from ducktools.classbuilder import AnnotationClass, Fieldclass _RegisterDescriptor:
def __init__(self, func, registry):
self.func = func
self.registry = registrydef __set_name__(self, owner, name):
self.registry.register(owner, self.func)
setattr(owner, name, self.func)class SerializeRegister:
def __init__(self):
self.serializers = {}def register(self, cls, func):
self.serializers[cls] = funcdef register_method(self, method):
return _RegisterDescriptor(method, self)def default(self, o):
try:
return self.serializers[type(o)](o)
except KeyError:
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")register = SerializeRegister()
@dataclass(slots=True)
class DataCoords:
x: float = 0.0
y: float = 0.0@register.register_method
def to_json(self):
return {"x": self.x, "y": self.y}# slots=True is the default for AnnotationClass
class BuilderCoords(AnnotationClass, slots=True):
x: float = 0.0
y: float = Field(default=0.0, doc="y coordinate")@register.register_method
def to_json(self):
return {"x": self.x, "y": self.y}# In both cases __slots__ have been defined
print(f"{DataCoords.__slots__ = }")
print(f"{BuilderCoords.__slots__ = }\n")data_ex = DataCoords()
builder_ex = BuilderCoords()objs = [data_ex, builder_ex]
print(data_ex)
print(builder_ex)
print()# Demonstrate you can not set values not defined in slots
for obj in objs:
try:
obj.z = 1.0
except AttributeError as e:
print(e)
print()print("Attempt to serialize:")
for obj in objs:
try:
print(f"{type(obj).__name__}: {json.dumps(obj, default=register.default)}")
except TypeError as e:
print(f"{type(obj).__name__}: {e!r}")
```Output (Python 3.12):
```
DataCoords.__slots__ = ('x', 'y')
BuilderCoords.__slots__ = {'x': None, 'y': 'y coordinate'}DataCoords(x=0.0, y=0.0)
BuilderCoords(x=0.0, y=0.0)'DataCoords' object has no attribute 'z'
'BuilderCoords' object has no attribute 'z'Attempt to serialize:
DataCoords: TypeError('Object of type DataCoords is not JSON serializable')
BuilderCoords: {"x": 0.0, "y": 0.0}
```## What features does this have? ##
Included as an example implementation, the `slotclass` generator supports
`default_factory` for creating mutable defaults like lists, dicts etc.
It also supports default values that are not builtins (try this on
[Cluegen](https://github.com/dabeaz/cluegen)).It will copy values provided as the `type` to `Field` into the
`__annotations__` dictionary of the class.
Values provided to `doc` will be placed in the final `__slots__`
field so they are present on the class if `help(...)` is called.`AnnotationClass` offers the same features with additional methods of gathering
fields.If you want something with more features you can look at the `prefab`
submodule which provides more specific features that differ further from the
behaviour of `dataclasses`.## Will you add \ to `classbuilder.prefab`? ##
No. Not unless it's something I need or find interesting.
The original version of `prefab_classes` was intended to have every feature
anybody could possibly require, but this is no longer the case with this
rebuilt version.I will fix bugs (assuming they're not actually intended behaviour).
However the whole goal of this module is if you want to have a class generator
with a specific feature, you can create or add it yourself.## Credit ##
Heavily inspired by [David Beazley's Cluegen](https://github.com/dabeaz/cluegen)
[^1]: `SlotFields` is actually just a subclassed `dict` with no changes. `__slots__`
works with dictionaries using the values of the keys, while fields are normally
used for documentation.[^2]: or `@attrs.define`.