Adding a node type#
Supporting a new piece of Python syntax touches the parser, the base renderer and,
usually, each built-in renderer. This page walks through the steps with a real example:
conditional expressions (x = a if test else b), the IfExp node. The snippets are
simplified; each step links to the code it describes.
1. Define the node#
Add a dataclass next to its relatives. An expression goes in
inline_nodes; a statement in line_nodes;
a compound statement in block_nodes.
@dataclass
class IfExp(HcInlineNode):
test: Any = None
body: Any = None
orelse: Any = None
is_true: Optional[bool] = None
type: str = "if_exp"
The type string is the name renderers register against: lowercase snake case, unique.
The real node is IfExp.
2. Parse it#
Add a branch to
AST_Parser.ast_parse. Recurse
into children with self.ast_parse, and capture whatever runtime information the
renderer needs at this point, since the namespace is only available here:
elif isinstance(node, ast.IfExp):
val = IfExp(
test=self.ast_parse(node.test),
body=self.ast_parse(node.body),
orelse=self.ast_parse(node.orelse),
is_true=self._evaluate_test(node.test),
)
Add a parser test in tests/test_parser.py that checks the node tree for a few sources.
3. Render it in BaseRenderer#
Register a handler in base, so every renderer can render the
node, even if plainly:
@BaseRenderer.register("if_exp")
def render_if_exp(renderer, node, base_context):
if_kw, else_kw = renderer.IF_EXP_KEYWORDS
body = renderer.render(node.body, base_context)
test = renderer.render(node.test, base_context)
orelse = renderer.render(node.orelse, base_context)
return f"{body}{if_kw}{test}{else_kw}{orelse}"
The real handler, render_if_exp, also
parenthesizes a nested conditional expression. The keywords come from a class attribute,
IF_EXP_KEYWORDS ((" if ", " else ") on BaseRenderer), so a renderer can change
them without a new handler.
Check whether the new node interacts with existing logic: precedence (OP_PRECEDENCE),
param-line detection (is_literal_expr), the duplicate-column check in
render_calcline. Add node-level tests in the style of tests/test_render_nodes.py,
using the renderer and make_context fixtures from tests/conftest.py.
4. Render it in each built-in renderer#
Decide how each format should show it, and register a handler or rule:
ASCIIRenderer: often the base handler is enough.LatexRenderer(and soMarkdownRenderer): math notation. ForIfExp, the LaTeX renderer only overridesIF_EXP_KEYWORDSwith upright\mathrm{if}and\mathrm{else}.HTMLRenderer: HTML markup.ExcelRenderer: an Excel formula (IfExpbecomesIF(...), seeformulas), or add it to the incompatible list so it raisesExcelRendererIncompatibleErrorwith a clear message.
5. Update the golden files and docs#
If the demo uses the new syntax, regenerate the golden files (see Tests). Add the node to Nodes and the node type reference, and describe the feature in How handcalcs reads your code.