pyparsing arithmetic grammar with infix_notation: working versions (tested 2026-10-16)
Parse and evaluate arithmetic expressions with pyparsing infix_notation, wiring operator precedence and associativity to parse actions, with variable lookup.
passed6 passed · 0 failed2026-10-16 21:57 UTC- Status
- passed
- Last passed
- 2026-10-16 21:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
pyparsing@3.3.3pytest@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 38972656245
The code
Exactly the files that passed. Your agent can pull them with get_recipe("pyparsing-arith-expression-grammar") (install), or see them on GitHub.
requirements.txt
pyparsing==3.3.3 pytest==9.1.1
grammar.py
import operator
from pyparsing import (
OpAssoc,
ParserElement,
Word,
alphanums,
alphas,
infix_notation,
one_of,
)
ParserElement.enable_packrat()
_BINARY = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv,
"^": operator.pow,
}
def _fold_binary(tokens):
terms = tokens[0]
value = terms[0]
for op, rhs in zip(terms[1::2], terms[2::2]):
value = _BINARY[op](value, rhs)
return value
def _fold_unary(tokens):
terms = tokens[0]
value = terms[-1]
for _ in terms[:-1]:
value = -value
return value
def _resolve(variables):
def lookup(tokens):
name = tokens[0]
if name not in variables:
raise ValueError(f"undefined variable: {name}")
return variables[name]
return lookup
def build_parser(variables=None):
variables = variables or {}
number = Word("0123456789").set_parse_action(lambda t: float(t[0]))
variable = Word(alphas + "_", alphanums + "_").set_parse_action(_resolve(variables))
operand = number | variable
return infix_notation(
operand,
[
(one_of("+ -"), 1, OpAssoc.RIGHT, _fold_unary),
(one_of("^"), 2, OpAssoc.RIGHT, _fold_binary),
(one_of("* /"), 2, OpAssoc.LEFT, _fold_binary),
(one_of("+ -"), 2, OpAssoc.LEFT, _fold_binary),
],
)
def evaluate(expression, **variables):
return build_parser(variables).parse_string(expression, parse_all=True)[0]
tests/test_grammar.py
import pytest
from pyparsing import ParseException
from grammar import build_parser, evaluate
def test_precedence_and_parentheses():
assert evaluate("2 + 3 * 4") == 14
assert evaluate("(2 + 3) * 4") == 20
def test_exponent_is_right_associative():
assert evaluate("2 ^ 3 ^ 2") == 512
def test_unary_minus_binds_tighter_than_power():
assert evaluate("-2 ^ 2") == 4
assert evaluate("10 - -5") == 15
def test_variables_are_substituted():
assert evaluate("price * qty + 1", price=2.5, qty=4) == 11.0
def test_undefined_variable_raises():
with pytest.raises(ValueError, match="undefined variable: x"):
evaluate("x + 1")
def test_parse_all_rejects_trailing_garbage():
parser = build_parser()
assert parser.parse_string("1 + 2 ignored")[0] == 3
with pytest.raises(ParseException):
evaluate("1 + 2 ignored")
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.