Custom nodes
Extend the library by defining a new node dataclass and matching a visitor, then register both in the dispatcher.
Step 1 — Define the node
Subclass Node. Keep it frozen and hashable (the default for @dataclass(slots=True, frozen=True, kw_only=True, unsafe_hash=True)):
from dataclasses import dataclass
from dynamic_expressions.nodes import Node
@dataclass(slots=True, frozen=True, kw_only=True, unsafe_hash=True)
class UppercaseNode(Node):
text: Node
Step 2 — Implement the visitor
Subclass Visitor and use dispatch to evaluate child nodes:
from dynamic_expressions.visitors import Dispatch, Visitor
class UppercaseVisitor(Visitor[UppercaseNode, object]):
async def visit(
self,
*,
node: UppercaseNode,
dispatch: Dispatch[object],
context: object,
) -> str:
value = await dispatch(node.text, context)
return str(value).upper()
Step 3 — Register in the dispatcher
import asyncio
from dynamic_expressions.dispatcher import VisitorDispatcher
from dynamic_expressions.nodes import LiteralNode
from dynamic_expressions.visitors import LiteralVisitor
async def main() -> None:
dispatcher = VisitorDispatcher(
visitors={
LiteralNode: LiteralVisitor(),
UppercaseNode: UppercaseVisitor(),
},
)
node = UppercaseNode(text=LiteralNode(value="hello"))
assert await dispatcher.visit(node, None) == "HELLO"
asyncio.run(main())
Each node type that can appear in your tree must have a registered visitor. Missing registrations raise KeyError at evaluation time.
Serialization
Built-in Pydantic and AST parsers recognize only standard nodes. For custom nodes:
- Add a Pydantic schema with
to_node()and register it inPydanticExpressionParser(types=...). - Or add an
ExpressionHandlerfor the AST parser.
Optional: DSL integration
To use custom nodes from the context DSL, wrap them in Expression manually or add helper functions in your application layer: