Prefab - A prebuilt classbuilder implementation
This is a more full featured dataclass replacement with some different design decisions and features.
Including:
Declaration by type hints, slots or
attribute(...)assignment on the class__prefab_pre_init__and__prefab_post_init__detection to allow for validation/conversionOptional
as_dictmethod generation to convert to a dictionary
Usage
Define the class using plain assignment and attribute function calls:
from ducktools.classbuilder.prefab import prefab, attribute
@prefab
class Settings:
hostname = attribute(default="localhost")
template_folder = attribute(default='base/path')
template_name = attribute(default='index')
Or with type hints:
from ducktools.classbuilder.prefab import prefab
@prefab
class Settings:
hostname: str = "localhost"
template_folder: str = 'base/path'
template_name: str = 'index'
In either case the result behaves the same.
>>> s = Settings()
>>> print(s)
Settings(hostname='localhost', template_folder='base/path', template_name='index')
Slots
Pre-slotted classes can be created by using the Prefab base class
from ducktools.classbuilder.prefab import Prefab
class Settings(Prefab):
hostname: str = "localhost"
template_folder: str = 'base/path'
template_name: str = 'index'
Why not just use attrs or dataclasses?
If attrs or dataclasses solves your problem then you should use them. They are thoroughly tested, well supported packages. This is a new project and has not had the rigorous real world testing of either of those.
This module has been created for situations where startup time is important,
such as for CLI tools and for handling conversion of inputs in a way that
was more useful to me than attrs converters (__prefab_post_init__).
Pre and Post Init Methods
Alongside the standard method generation @prefab decorated classes
have special behaviour if __prefab_pre_init__ or __prefab_post_init__
methods are defined.
For both methods if they have additional arguments with names that match
defined attributes, the matching arguments to __init__ will be passed
through to the method.
If an argument is passed to __prefab_post_init__it will not be initialized
in __init__. It is expected that initialization will occur in the method
defined by the user.
Other than this, arguments provided to pre/post init do not modify the behaviour of their corresponding attributes (they will still appear in the other magic methods).
Examples have had repr and eq removed for brevity.
Examples
__prefab_pre_init__
Input code:
from ducktools.classbuilder.prefab import prefab
@prefab(repr=False, eq=False)
class ExampleValidate:
x: int
@staticmethod
def __prefab_pre_init__(x):
if x <= 0:
raise ValueError("x must be a positive integer")
Equivalent code:
class ExampleValidate:
PREFAB_FIELDS = ['x']
__match_args__ = ('x',)
def __init__(self, x: int):
self.__prefab_pre_init__(x=x)
self.x = x
@staticmethod
def __prefab_pre_init__(x):
if x <= 0:
raise ValueError('x must be a positive integer')
__prefab_post_init__
Input code:
from ducktools.classbuilder.prefab import prefab, attribute
from pathlib import Path
@prefab(repr=False, eq=False)
class ExampleConvert:
x: Path = attribute(default='path/to/source')
def __prefab_post_init__(self, x: Path | str):
self.x = Path(x)
Equivalent code:
from pathlib import Path
class ExampleConvert:
PREFAB_FIELDS = ['x']
__match_args__ = ('x',)
x: Path
def __init__(self, x: Path | str = 'path/to/source'):
self.__prefab_post_init__(x=x)
def __prefab_post_init__(self, x: Path | str):
self.x = Path(x)
Differences with dataclasses
While this project doesn’t intend to exactly replicate other similar modules it’s worth noting where they differ in case users get tripped up.
Prefabs don’t behave quite the same (externally) as dataclasses. They are very different internally.
This doesn’t include things that haven’t been implemented, and only focuses on intentional differences. Unintentional differences may be patched or will be added to this list.
Functional differences
the
as_dictmethod inprefab_classesdoes not behave the same as dataclasses’asdict.as_dictdoes not deepcopy the included fields, modification of mutable fields in the dictionary will modify them in the original object.as_dictdoes not recurseRecursion would require knowing how other objects should be serialized.
dataclasses
asdict’s recursion appears to be for handling json serialization prefab expects the json serializer to handle recursion.
dataclasses provides a
fieldsfunction to access the underlying fields.prefabuses aget_attributesfunction to return the attributes as a dict.
Plain
attribute(...)declarations can be used without the use of type hints.If a plain assignment is used, all assignments must use
attribute.
Post init processing uses
__prefab_post_init__instead of__post_init__This is just a case of not wanting any confusion between the two.
attrssimilarly does__attrs_post_init__.__prefab_pre_init__can also be used to define something to run before the body of__init__.If an attribute name is provided as an argument to either the pre_init or post_init functions the value will be passed through.
Unlike dataclasses, prefab classes will let you use unhashable default values.
This isn’t to say that mutable defaults are a good idea in general but prefabs are supposed to behave like regular classes and regular classes let you make this mistake.
Usually you should use
attribute(default_factory=list)or similar.
If
initisFalsein@prefab(init=False)the method is still generated but renamed to__prefab_init__.Slots are supported but not from annotations using the decorator
@prefabuse the
Prefabbase class if you wish your classes to be automatically slotted.@prefabcan be used if the slots are provided with a__slots__ = SlotFields(...)attribute set.The support for slots in
attrsanddataclassesinvolves recreating the class as it is not possible to effectively define__slots__after class creation. This can cause bugs where decorators or caches hold references to the original class.
InitVar annotations are not supported.
So far I haven’t needed this yet so it hasn’t been implemented.
The
__repr__method for prefabs will have a different output if it will notevalcorrectly.This isn’t a guarantee that the regular
__repr__will eval, but if it is known that the output would notevalthen an alternative repr is used which does not look like it wouldeval.
default_factory functions will be called if
Noneis passed as an argumentThis makes it easier to wrap the function.
The
Prefabbase class will automatically create the__dict__slot ifcached_propertyis used in the class.This means that cached properties will work as expected in slotted classes but that you will also be able to set any attribute in non-frozen classes