parsimonious PEG grammar with a NodeVisitor tree walker: working versions (tested 2026-10-18)
Define a recursive PEG arithmetic grammar, turn the parse tree into Python values with a NodeVisitor, and handle ParseError, IncompleteParseError and wrapped VisitationError.
passed6 passed · 0 failed2026-10-18 03:57 UTC- Status
- passed
- Last passed
- 2026-10-18 03:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
parsimonious@0.11.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 39095370682
The code
Exactly the files that passed. Your agent can pull them with get_recipe("parsimonious-peg-grammar") (install), or see them on GitHub.
requirements.txt
parsimonious==0.11.0 pytest==9.1.1
grammar.py
import operator
from parsimonious.grammar import Grammar
from parsimonious.nodes import NodeVisitor
GRAMMAR = Grammar(
r"""
expr = term (add_op term)*
term = factor (mul_op factor)*
factor = number / paren
paren = "(" expr ")"
add_op = "+" / "-"
mul_op = "*" / "/"
number = ~r"\d+"
"""
)
OPS = {"+": operator.add, "-": operator.sub, "*": operator.mul, "/": operator.truediv}
class Calc(NodeVisitor):
grammar = GRAMMAR
def generic_visit(self, node, children):
# Literals and wrapper nodes just pass their children through.
return children or node
def visit_expr(self, node, children):
value, rest = children
for op, term in rest:
value = op(value, term)
return value
def visit_term(self, node, children):
value, rest = children
for op, factor in rest:
value = op(value, factor)
return value
def visit_factor(self, node, children):
return children[0]
def visit_paren(self, node, children):
return children[1]
def visit_add_op(self, node, children):
return OPS[node.text]
def visit_mul_op(self, node, children):
return OPS[node.text]
def visit_number(self, node, children):
return int(node.text)
def evaluate(text):
return Calc().parse(text)
tests/test_grammar.py
import pytest
from parsimonious.exceptions import (IncompleteParseError, ParseError,
VisitationError)
from grammar import evaluate
def test_evaluates_precedence_and_parens():
assert evaluate("2+3*4") == 14
assert evaluate("(2+3)*4") == 20
assert evaluate("7/2") == 3.5
def test_subtraction_is_left_associative():
assert evaluate("10-3-2") == 5
def test_trailing_input_raises_incomplete_parse_error():
assert issubclass(IncompleteParseError, ParseError)
with pytest.raises(IncompleteParseError):
evaluate("1+2)")
def test_leading_garbage_raises_plain_parse_error():
with pytest.raises(ParseError) as exc:
evaluate("+1")
assert not isinstance(exc.value, IncompleteParseError)
def test_whitespace_is_not_implicitly_skipped():
with pytest.raises(IncompleteParseError):
evaluate("1 + 2")
def test_visitor_exceptions_are_wrapped():
with pytest.raises(VisitationError):
evaluate("1/0")
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.