Python define simple expression language (bash like)

51 Views Asked by At

I want to define my own simple language for evaluating literals.

Requirements:

  • Based on python to allow easy use/addition of Python functions (not shown in the example)
  • Token should be iterpreted as Literal if not preceded by $.
  • $ before token means it is a Variable (Similar to bash)

TL;DR:

I want dollar_solution(**VAR_DICT) to return "Minimum Working Example"


Due diligence (Ignore if not helpful):

  • So far, I have been able to accomplish this but with the special character being Unary Subtraction (USub [-]).
  • The part (of dollar_solution) that fails is during ast.parse. Looking at the code for ast.parse, it just calls the builtins:compile. Does this mean I need to call my own compile with my own .asdl?
  • Half-baked idea based on my research (in order of cleanliness):
    • Define unary operator $ (in .asdl) and handle it in MyTransormer::visit_UnaryOp.
    • Define my own .asdl to allow dollar as a valid character in names and handle it in MyTransformer
  • It smells like I might need to define my own language. Where do I start with that with a parser in Python?
  • Is there some library that does that already?
import ast
from _ast import Name, Constant, BinOp, Add, UnaryOp, USub
from typing import Any

EXPECTED_RESULT = "Minimum Working Example"

VAR_DICT = {"mini": "Minimum", "ex": "Example"}

MINUS_EXPRESSION = "-mini + ' ' + Working + ' ' + -ex"
DOLLAR_EXPRESSION = "$mini + ' ' + Working + ' ' + $ex"


def minus_solution(**kwargs):
    tree = ast.parse(MINUS_EXPRESSION, mode="eval")
    safe_tree = MyTransformer(var_dict=kwargs).visit(tree)
    return ast.literal_eval(safe_tree)


def dollar_solution(**kwargs):
    tree = ast.parse(DOLLAR_EXPRESSION, mode="eval")
    safe_tree = MyTransformer(var_dict=kwargs).visit(tree)
    return ast.literal_eval(safe_tree)


def main():
    try:
        assert dollar_solution(**VAR_DICT) == EXPECTED_RESULT
    except:
        print("Dollar Solution Failed!!!")
    else:
        print("Dollar Solution Success!!!")

    try:
        assert minus_solution(**VAR_DICT) == EXPECTED_RESULT
    except:
        print("Minus Solution Failed!!!")
    else:
        print("Minus Solution Success!!!")


class MyTransformer(ast.NodeTransformer):
    def __init__(self, var_dict):
        self._var_dict = var_dict

    def visit_Name(self, node: Name) -> Any:
        return Constant(
            value=node.id,
            col_offset=node.col_offset,
            lineno=node.lineno,
        )

    def visit_BinOp(self, node: BinOp) -> Any:
        super().generic_visit(node)
        assert (
            isinstance(node.op, Add)
            and isinstance(node.left, Constant)
            and isinstance(node.right, Constant)
        ), f"Bad operand or operator"

        return Constant(
            value=str(node.left.value) + str(node.right.value),
            col_offset=node.col_offset,
            lineno=node.lineno,
        )

    def visit_UnaryOp(self, node: UnaryOp) -> Any:
        assert isinstance(node.operand, Name) and isinstance(
            node.op, USub
        ), "Not Name in expression"
        return Constant(
            value=self._var_dict[node.operand.id],
            col_offset=node.col_offset,
            lineno=node.lineno,
        )


if __name__ == "__main__":
    main()

0

There are 0 best solutions below