3.2.2.5. Templates#
Changed in version 2.9.0: Added support for Python’s builtin TypeVar`
Templates are generic model classes that become concrete struct classes after
specialization. A template class stores template metadata on __template__
and does not store __struct__ until it is specialized.
Caterpillar supports two template systems:
Python generic templates, the preferred API for new code.
Legacy
TemplateTypeVartemplates, kept for compatibility.
Python Generic Templates#
Generic templates use normal Python TypeVar objects and Generic bases.
from typing import Generic, TypeVar
from caterpillar.py import template, uint8
T = TypeVar("T")
@template
class Box(Generic[T]):
value: T
ByteBox = Box[uint8]
Box is a template. Box[uint8] is a concrete struct class. Caterpillar
installs __class_getitem__ on the template class and materializes the
specialization when it is subscripted.
Specialization performs two different substitutions:
In normal Python type positions, Caterpillar field objects are replaced with their Python value type using
typeof().In field metadata positions, Caterpillar keeps the actual field object and builds a
Fieldfrom it.
For example:
@template
class Box(Generic[T]):
value: T
ByteBox = Box[uint8]
value: T is converted to a Field(uint8) before the generated class is
passed to Struct.
Layout Metadata With f[]
f[] is Caterpillar’s public spelling for typing.Annotated[]. Use it
when the Python value type and the binary layout metadata must both be present.
from typing import Generic, TypeVar
from caterpillar.py import f, field_of, template, uint8
T = TypeVar("T")
@template
class Vector(Generic[T]):
values: f[list[T], field_of(T)[2]]
ByteVector = Vector[uint8]
In this example, the Python-facing type becomes list[int] while the binary
layout metadata becomes a two-element Field(uint8).
field_of(T) supports the same layout operators as
TemplateTypeVar: sequence length, offset, switch
options, byte order, bit width, and condition. Generic specializations are cached
on the template origin. Repeating the same specialization returns the same class:
assert Box[uint8] is Box[uint8]
Generated classes store Caterpillar-owned metadata:
__origin__The template class that produced the specialization.
__args__The concrete specialization arguments.
Because Box[uint8] returns a real class at runtime, it is not a
typing generic alias after materialization. Use the metadata above instead
of typing.get_origin() and typing.get_args() for runtime inspection.
Partial Generic Templates#
If a specialization still contains unresolved type variables, Caterpillar keeps the result as a template.
from typing import Generic, TypeVar
from caterpillar.py import template, uint8, uint16
T = TypeVar("T")
U = TypeVar("U")
@template
class Pair(Generic[T, U]):
left: T
right: U
BytePair = Pair[uint8, U]
ByteWordPair = BytePair[uint16]
BytePair is a template. ByteWordPair is a concrete struct class.
Legacy Template Variables#
Legacy templates use TemplateTypeVar or string
names in the decorator.
from caterpillar.py import TemplateTypeVar, derive, template, uint8, uint16
A = TemplateTypeVar("A")
@template(A, "B")
class FormatTemplate:
foo: A
bar: B
Format = derive(FormatTemplate, uint8, uint16)
Legacy templates classify parameters as required or keyword-only defaults:
Required parameters are passed positionally or by keyword to
derive().Keyword defaults are declared in
@template(T=uint8)and may be omitted fromderive().
The legacy decorator temporarily injects missing template names into the caller module while annotations are evaluated. This keeps legacy templates compatible with deferred annotation evaluation.
derive()#
derive() remains available for both template systems.
For legacy templates, derive() is the primary specialization API. For
generic templates, direct subscript syntax is preferred, but derive() can be
used when a name or union option must be supplied explicitly.
NamedByteBox = derive(Box, uint8, name="NamedByteBox")
Passing an already materialized struct class to derive() without additional
arguments returns that class unchanged.
Type Checking#
Static type checkers see generic templates as ordinary Python generic classes.
At runtime, Caterpillar replaces template arguments with concrete binary
layouts. If a project needs precise static typing for field atoms such as
uint8, expose typing-only aliases to their Python value types while keeping
the runtime field objects unchanged.
Developer’s note
Template specialization is performed once when a concrete class is created.
Pack and unpack operations use the normal Struct and Field paths.