Custom Formatters¶
liblaf.pretty gives you four ways to customize formatting:
- Implement
__pretty__(self, ctx)when you own the type. - Use
register_type()for one concrete class. - Use
register_func()for structural handlers that may or may not apply. - Use
register_lazy()when the target type lives in an optional dependency.
Choose A Hook¶
Use the smallest hook that matches the problem:
__pretty__()keeps the formatting logic next to the model you own.register_type()is the normal choice for a concrete third-party class.register_func()is useful when you need structural matching instead of type matching.register_lazy()keeps optional integrations cheap because it waits for the dependency to be imported elsewhere.
Builtin handlers already cover core containers, fieldz-compatible models, and
__rich_repr__, so reach for a custom hook only when the default behavior is
not enough.
Resolution Order¶
The registry checks handlers in this order:
obj.__pretty__(ctx)when presentregister_type()handlers, including subclass matchesregister_func()handlers in reverse registration order- fallback repr-based formatting
Returning None from __pretty__() or register_func() lets the formatter
continue to the next option.
A __pretty__() Method¶
If you control the class, __pretty__(self, ctx) keeps the formatting logic
next to the model:
from rich.text import Text
class Point:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def __pretty__(self, ctx):
return ctx.container(
obj=self,
begin=Text("(", "repr.tag_start"),
children=[ctx.name_value("x", self.x), ctx.name_value("y", self.y)],
end=Text(")", "repr.tag_end"),
)
The hook receives only ctx. Older ctx, depth examples are stale for this
package.
If __pretty__() returns None, the registry keeps looking for other
handlers.
A Concrete Type¶
from rich.text import Text
from liblaf.pretty import pformat, register_type
class Point:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
@register_type(Point)
def _pretty_point(obj: Point, ctx):
return ctx.container(
obj=obj,
begin=Text("(", "repr.tag_start"),
children=[ctx.name_value("x", obj.x), ctx.name_value("y", obj.y)],
end=Text(")", "repr.tag_end"),
)
print(pformat(Point(1, 2)).to_plain(), end="")
ctx.container() prefixes the object's type name automatically for
referencable objects, so begin and end usually only need delimiters.
register_type() uses functools.singledispatch, so subclasses also match
unless you register something more specific.
Building Output With PrettyContext¶
PrettyContext exposes a few helpers that are enough for most handlers:
ctx.leaf(obj, text)builds a scalar node from arich.text.Textvalue.ctx.positional(value)wraps one positional child.ctx.name_value(name, value)buildsname=valueoutput.ctx.key_value(key, value)buildskey: valueoutput.ctx.container(obj=..., begin=..., children=..., end=...)builds repr-like tagged containers.
Use Text for begin, end, and custom leaf output. Those values flow
through the Rich rendering pipeline and are not plain strings.
ctx.container() is referencable by default, which means repeated appearances
of the same object can later collapse into shared-reference tags. Use
ctx.leaf(..., referencable=False) or ctx.container(..., referencable=False)
for inline summaries that should always render as a value.
ctx.name_value(name, value) falls back to positional output when name is
falsey. That matches the built-in __rich_repr__ adapter, where ("", value)
and (None, value) are treated like positional items.
ctx.container() adds repr-style commas and spaces by default. Pass
add_separators=False when you want full control over child punctuation, or
empty=... when the empty rendering should not be just begin + end.
Helper Utilities¶
PrettyContext also exposes a few utility helpers that are useful in custom
handlers:
ctx.truncate_list(items)yields values up tomax_list, then a truncation marker.ctx.truncate_dict(items)yields key-value pairs up tomax_dict, then a truncation marker.ctx.possibly_sorted(items)sorts orderable values and preserves original order when sorting would fail.ctx.add_separators(items)attaches repr-style commas and spaces to existing wrapped items.
These helpers are how the built-in container handlers keep behavior aligned with the public configuration surface.
Structural Registration¶
register_func() is useful when the handler should inspect an object and decide
at runtime whether it applies. Return None to let the next handler try.
Functional handlers run after type-based handlers and are checked in reverse
registration order, so the most recently registered handler wins.
Lazy Registration¶
register_lazy(module, name) defers registration until that module has already
been imported. It does not import the module for you. This is a good fit for
optional dependencies such as array or tensor types that should not be imported
just for pretty-printing.
from rich.text import Text
from liblaf.pretty import register_lazy
@register_lazy("numpy", "ndarray")
def _pretty_ndarray(obj, ctx):
return ctx.leaf(
obj,
Text(f"ndarray(shape={obj.shape!r}, dtype={obj.dtype!s})", "repr.tag_name"),
referencable=False,
)
Once numpy is present in sys.modules, the handler is resolved and cached for
future formatting calls.
When To Mark Things Referencable¶
Containers built with ctx.container() participate in shared-reference
tracking by default. That is usually what you want for mappings, sets,
frozensets, and object-like containers.
Use referencable=False when a formatter is really just an inline summary.
Builtin scalar handlers and list-like sequence handlers use that path so they
always render their value instead of collapsing into <Type @ hexid> markers.