ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.