Custom Constructors and Representers#
This guide explains how to create custom YAML constructors and representers with yayaml.
Overview#
Constructors convert YAML nodes into Python objects during loading
Representers convert Python objects into YAML nodes during dumping
yayaml provides decorators and helper functions to simplify creating both.
Custom Constructors#
Using the @is_constructor Decorator#
The simplest way to add a custom constructor is with the @is_constructor decorator:
from yayaml import is_constructor, yaml
@is_constructor("!point")
def construct_point(loader, node):
"""Construct a Point from a YAML sequence [x, y]."""
coords = loader.construct_sequence(node, deep=True)
return Point(coords[0], coords[1])
Now you can use the tag in YAML:
location: !point [10, 20]
Constructor with Aliases#
You can register multiple tags for the same constructor:
@is_constructor("!pt", aliases=("!point", "!coord"))
def construct_point(loader, node):
coords = loader.construct_sequence(node, deep=True)
return Point(coords[0], coords[1])
Constructor with Error Hints#
Add helpful hints that appear in error messages:
@is_constructor("!point", hint="Expected sequence [x, y] for !point tag")
def construct_point(loader, node):
coords = loader.construct_sequence(node, deep=True)
if len(coords) != 2:
raise ValueError("Point requires exactly 2 coordinates")
return Point(coords[0], coords[1])
Using add_constructor Directly#
For simple cases, you can use add_constructor without a decorator:
from yayaml import add_constructor, yaml
def construct_uppercase(loader, node):
return loader.construct_scalar(node).upper()
add_constructor("!upper", construct_uppercase)
Using construct_from_func#
For constructors that simply apply a function to the node value, use construct_from_func:
from functools import partial
from yayaml import add_constructor, construct_from_func
# Create a constructor that applies `str.upper` to the scalar value
add_constructor(
"!upper",
partial(construct_from_func, func=str.upper, unpack=False)
)
The unpack parameter controls argument handling:
unpack=False: Pass the constructed value directly to the functionunpack=True: Unpack sequences as*argsand mappings as**kwargs
Example with Unpacking#
from functools import partial
from yayaml import add_constructor, construct_from_func
def create_rectangle(width, height):
return {"width": width, "height": height, "area": width * height}
add_constructor(
"!rect",
partial(construct_from_func, func=create_rectangle, unpack=True)
)
Usage:
# As sequence (unpacked as positional args)
box1: !rect [10, 20]
# As mapping (unpacked as keyword args)
box2: !rect
width: 10
height: 20
Custom Representers#
Using the @is_representer Decorator#
from yayaml import is_representer, yaml
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@is_representer(Point)
def represent_point(representer, point, *, tag):
return representer.represent_sequence(tag, [point.x, point.y])
Now when you dump a Point, it will be serialized:
from yayaml import yaml_dumps
pt = Point(10, 20)
print(yaml_dumps({"location": pt}))
# Output:
# location: !Point [10, 20]
Custom Tag Name#
By default, the tag is derived from the class name. You can customize it:
@is_representer(Point, tag="!pt")
def represent_point(representer, point, *, tag):
return representer.represent_sequence(tag, [point.x, point.y])
Using add_representer Directly#
from yayaml import add_representer, yaml
def represent_point(representer, point, *, tag):
return representer.represent_mapping(tag, {"x": point.x, "y": point.y})
add_representer(Point, represent_point, tag="!point")
Using build_representer#
For simple cases where you just need to convert an object to a serializable form, use build_representer:
from yayaml import add_representer, build_representer
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# Represent as a sequence [x, y]
add_representer(
Point,
build_representer(lambda pt: [pt.x, pt.y])
)
# Or represent as a mapping {x: ..., y: ...}
add_representer(
Point,
build_representer(lambda pt: {"x": pt.x, "y": pt.y})
)
# Or represent as a scalar (string)
add_representer(
Point,
build_representer(lambda pt: f"({pt.x}, {pt.y})")
)
The build_representer function automatically determines the representation type based on what your simplification function returns:
listortuple-> sequence nodedict-> mapping nodeanything else -> scalar node
Complete Example#
Here’s a complete example showing both a constructor and representer for a custom class:
from yayaml import (
is_constructor,
is_representer,
yaml,
yaml_dumps,
load_yml,
write_yml,
)
class Color:
def __init__(self, r, g, b):
self.r = r
self.g = g
self.b = b
def __repr__(self):
return f"Color({self.r}, {self.g}, {self.b})"
# Register constructor
@is_constructor("!color")
def construct_color(loader, node):
values = loader.construct_sequence(node, deep=True)
return Color(*values)
# Register representer
@is_representer(Color, tag="!color")
def represent_color(representer, color, *, tag):
return representer.represent_sequence(tag, [color.r, color.g, color.b])
# Usage
yaml_content = """
background: !color [255, 255, 255]
foreground: !color [0, 0, 0]
"""
data = yaml.load(yaml_content)
print(data["background"]) # Color(255, 255, 255)
# Round-trip
output = yaml_dumps(data)
print(output)
# background: !color [255, 255, 255]
# foreground: !color [0, 0, 0]
Working with Different Node Types#
YAML has three node types that your constructors may need to handle:
Scalar Nodes#
Single values (strings, numbers, booleans, null):
@is_constructor("!upper")
def construct_upper(loader, node):
# For scalar nodes, use construct_scalar
return loader.construct_scalar(node).upper()
Sequence Nodes#
Lists/arrays:
@is_constructor("!sorted")
def construct_sorted(loader, node):
# For sequence nodes, use construct_sequence
items = loader.construct_sequence(node, deep=True)
return sorted(items)
Mapping Nodes#
Dictionaries/objects:
@is_constructor("!config")
def construct_config(loader, node):
# For mapping nodes, use construct_mapping
data = loader.construct_mapping(node, deep=True)
return Config(**data)
Handling Multiple Node Types#
Use scalar_node_to_object for flexible scalar handling:
from yayaml import scalar_node_to_object
import ruamel.yaml
@is_constructor("!flexible")
def construct_flexible(loader, node):
if isinstance(node, ruamel.yaml.nodes.ScalarNode):
return scalar_node_to_object(loader, node)
elif isinstance(node, ruamel.yaml.nodes.SequenceNode):
return loader.construct_sequence(node, deep=True)
elif isinstance(node, ruamel.yaml.nodes.MappingNode):
return loader.construct_mapping(node, deep=True)
Registering with Different YAML Instances#
By default, constructors and representers are registered with the main yaml instance from yayaml. You can specify a different instance:
from yayaml import is_constructor, is_representer, yaml, yaml_safe
# Register with the safe yaml instance
@is_constructor("!mytag", _yaml=yaml_safe)
def construct_mytag(loader, node):
...
# Register with the main yaml instance (default)
@is_representer(MyClass, _yaml=yaml)
def represent_myclass(representer, obj, *, tag):
...