Tutorial: Making a class boilerplate generator
The core idea is that there are 4 parts to the process of generating the class boilerplate that need to be handled:
Create a new subclass of
Fieldif you need to add any extra attributes to fieldsMake a new gatherer function or use one of the tools provided to create a generator that will use your new
Fieldsubclass (ex:make_unified_gatherer(NewField))Write any code generator functions you wish to add or modify for your class
Create a function or base class that applies these to classes using the
builderprovided
The field gathering should not attempt to do any inheritance checking, that is already handled
by the builder function. slot_gatherer is an example of a gatherer.
To demonstrate this we will go through making a new Field, gatherer, generated method, class decorator and base class.
Deciding on Customizations
For the purpose of this tutorial we are going to make a special
class generator which works by collecting fields from a __fields__
attribute and will generate the usual __init__, __repr__ and __eq__
functions alongside a new report method that will give a longer set of
details about a class instance.
The input will look like this:
class Example(ReportClass):
__fields__ = {
"x": 42,
"y": CustomField(default="value", report=False)
}
The final code from this tutorial is available in docs_code/tutorial_code.py
These are the imports required for all of the following steps:
from types import MappingProxyType
from pprint import pp
import ducktools.classbuilder as dtbuild
Step 1: Defining a Field subclass
The first step is to generate our new Field subclass to add the report
attribute that can be used to hide the value from the report if desired.
class CustomField(dtbuild.Field):
report: bool = True
That is it, this will now function as a field with the additional keyword only
parameter report added.
Step 2: Creating a new __fields__ gatherer
Having done that we now need to create a gatherer that will collect values from __fields__.
After a class has been built __fields__ will be set to None to indicate that the class has
been created and not to attempt to repeat the process.
def fields_attribute_gatherer(cls_or_ns):
# Gatherers need to work on either a class or a class namespace
# `builder` will operate on the class while `SlotMakerMeta` only has the namespace
if isinstance(cls_or_ns, (MappingProxyType, dict)):
cls_dict = cls_or_ns
else:
cls_dict = cls_or_ns.__dict__
cls_fields_attrib = cls_dict.get("__fields__", None)
if cls_fields_attrib is None:
raise AttributeError("Class has already been generated or `__fields__` has not been set")
elif not isinstance(cls_fields_attrib, dict):
raise TypeError("__fields__ attribute must be a dictionary")
gathered_fields = {}
# Field or CustomField instances will be copied and converted if needed
# Plain values will be made into CustomField instances
for k, v in cls_fields_attrib.items():
if isinstance(v, dtbuild.Field):
gathered_fields[k] = CustomField.from_field(v)
else:
gathered_fields[k] = CustomField(default=v)
# Modifications to be made to class attributes
modifications = {
"__fields__": None
}
return gathered_fields, modifications
Demonstrate the output of this gatherer:
class GathererTest:
__fields__ = {
"field_1": "First Field",
"field_2": CustomField(default="Second Field"),
"field_3": CustomField(default="Third Field", report=False),
}
pp(fields_attribute_gatherer(GathererTest))
Step 3: Define the ‘report’ code generator
For the example just demonstrated our report will generate output like this.
Class: GathererTest
field_1: "First Field"
field_2: "Second Field"
field_3: <HIDDEN>
def report_generator(cls, funcname="report"):
fields = dtbuild.get_fields(cls)
field_reports = []
for name, fld in fields.items():
if getattr(fld, "report", True):
field_reports.append(f"{name}: {{repr(self.{name})}}")
else:
field_reports.append(f"{name}: <HIDDEN>")
reports_str = "\\n".join(field_reports)
class_str = f"Class: {cls.__name__}"
code = (
"@property\n"
f"def {funcname}(self):\n"
f" return f\"{class_str}\\n{reports_str}\""
)
globs = {}
return dtbuild.GeneratedCode(code, globs)
report_maker = dtbuild.MethodMaker("report", report_generator)
We can take a quick look at what this generates by applying it to a slotclass:
@dtbuild.slotclass
class CodegenDemo:
__slots__ = dtbuild.SlotFields(
field_1="Field one",
field_2="Field two",
field_3="Field three",
)
field_1: str = "Field one"
field_2: str = "Field two"
field_3: str = "Field three"
print(report_generator(CodegenDemo).source_code)
Step 4: Create the builders
Here we will make both a simple decorator based builder and then a subclass
based builder that can create __slots__.
4a: Decorator builder
def reportclass(cls):
gatherer = fields_attribute_gatherer
methods = {
dtbuild.eq_maker,
dtbuild.repr_maker,
dtbuild.init_maker,
report_maker
}
slotted = "__slots__" in vars(cls)
flags = {"slotted": slotted}
return dtbuild.builder(cls, gatherer=gatherer, methods=methods, flags=flags)
4b: Base class Builder
# Once slots have been made, slot_gatherer should be used.
slot_gatherer = dtbuild.make_slot_gatherer(CustomField)
class ReportClass(metaclass=dtbuild.SlotMakerMeta, gatherer=fields_attribute_gatherer):
__slots__ = {}
def __init_subclass__(cls):
# Check if the metaclass has pre-gathered data
gatherer = fields_attribute_gatherer
methods = {
dtbuild.eq_maker,
dtbuild.repr_maker,
dtbuild.init_maker,
report_maker
}
# The class may still have slots unrelated to code generation
slotted = "__slots__" in vars(cls)
flags = {"slotted": slotted}
dtbuild.builder(cls, gatherer=gatherer, methods=methods, flags=flags)
Step 5: Try out the new class generators and method
Unslotted with decorator:
@reportclass
class Example:
__fields__ = {
"x": 42,
"y": CustomField(default="value", report=False)
}
print(Example().report)
Slotted with base class:
class ExampleSlots(ReportClass):
__fields__ = {
"x": 42,
"y": CustomField(default="value", report=False)
}
print(ExampleSlots().report)