Architecture#

This page follows one call, HandCalcs(renderer)(source), from source code to output. Each step links to the code that does it.

1. Run the code#

If no namespace is supplied, the interface runs the source with exec and keeps the resulting globals (HandCalcs.__call__). In Jupyter the notebook has already run the cell, and the decorator runs the function with innerscope. Either way the result is a dictionary of values.

2. Parse into nodes#

HcSequence.from_source turns the source into a tree:

  1. AST_Parser walks Python’s ast and builds handcalcs nodes: dataclasses such as CalcLine, Name and DivOp. Every Name is given its value from the namespace. Comments are classified as headings, comment lines, inline comments or commands (comment_parser), and # hc: flags are parsed with argparse.

  2. convert_if_tree turns each if/elif/else chain into an ElifBlock of IfBlocks (and an ElseBlock).

  3. group_lines gathers consecutive calculation lines into a CalcsBlock and consecutive comment lines into a CommentsBlock, so a renderer can treat a run of lines as one group (one LaTeX aligned environment, one HTML paragraph).

  4. set_levels records each node’s nesting depth on its level attribute.

The root of the tree is an HcSequence whose sequence holds the top-level nodes. See Nodes for every node type.

3. Create the render context#

renderer.create_context(**settings) builds a BaseRenderContext from the global configuration (and, for HandCalcs, a global_ns field holding the namespace). It has a global layer, for settings that persist, and a line layer, for settings that apply to the current line only. See The render context.

4. Render each node#

renderer.render(root, context) calls render_root, which renders each top-level node with render_node. For one node, render_node:

  1. runs the node type’s pre rules (each may return a changed node);

  2. runs its sym or num rules, depending on context.current_mode;

  3. calls the node type’s handler, which returns the rendered piece, and usually calls renderer.render(child, context) for the node’s children;

  4. runs its post rules on the rendered piece.

Handlers and rules are looked up by the node’s type string in tables that register fills. A calculation line is rendered twice per sub-expression: once with current_mode = "sym" (names as symbols) and once with "num" (names as values), by render_calcline.

The result is the render tree, a nested list mirroring the node tree. With the built-in handlers, a line becomes a list of strings and a block becomes [header, body]; see The render tree.

5. Complete#

renderer.complete(tree, context) produces the final output. Text renderers call join, which walks the tree with join_item and inserts spaces, newlines and indentation. Handlers deliberately leave all of that out, so each renderer decides its own layout in one place.

BaseRenderer.complete returns the tree unchanged; that is what you see from the base renderer.

Where the code lives#

Step

Module

Interfaces

object_oriented, jupyter, decorator

Parsing

ast_parser, sequence

Nodes

nodes, line_nodes, block_nodes, inline_nodes, operator_nodes

Rendering core

base

Built-in renderers

ascii, latex, markdown, html, excel

Configuration

config

Next: Nodes.