Source code for formulae.terms.call_resolver

import operator

from formulae.expr import Assign
from formulae.transforms import STATEFUL_TRANSFORMS


class CallResolverError(Exception):
    pass


[docs]class LazyOperator: """Unary and Binary lazy operators. Functions calls like ``a + b`` are converted into a LazyOperator that is resolved when you explicitly evaluates it. Parameters ---------- op: builtin_function_or_method An operator in the ``operator`` built-in module. It can be one of ``add``, ``pos``, ``sub``, ``neg``, ``pow``, ``mul``, and ``truediv``. args: One or two lazy instances. """ def __init__(self, op, *args): self.op = op self.args = args self.symbol = self._get_symbol() def __str__(self): if len(self.args) == 1: return f"{self.symbol}{self.args[0]}" else: return f"{self.args[0]} {self.symbol} {self.args[1]}" def __repr__(self): return self.__str__() def __hash__(self): return hash((self.symbol, *self.args)) def __eq__(self, other): if not isinstance(other, type(self)): return False return self.symbol == other.symbol and set(self.args) == set(other.args) def _get_symbol(self): oname = self.op.__name__ if oname in ["add", "pos"]: symbol = "+" elif oname in ["sub", "neg"]: symbol = "-" elif oname == "pow": symbol = "**" elif oname == "mul": symbol = "*" elif oname == "truediv": symbol = "/" return symbol def accept(self, visitor): return visitor.visitLazyOperator(self)
[docs] def eval(self, data_mask, eval_env): """Evaluates the operation. Evaluates the arguments involved in the operation, calls the Python operator, and returns the result. Parameters ---------- data_mask: pd.DataFrame The data frame where variables are taken from eval_env: EvalEnvironment The environment where values and functions are taken from. Returns ------- result: The value obtained from the operator call. """ return self.op(*[arg.eval(data_mask, eval_env) for arg in self.args])
[docs]class LazyVariable: """Lazy variable name. The variable represented in this object does not hold any value until it is explicitly evaluated within a data mask and an evaluation environment. Parameters ---------- name: str The name of the variable it represents. """ def __init__(self, name): self.name = name def __str__(self): return self.name def __repr__(self): return self.__str__() def __hash__(self): return hash((self.name)) def __eq__(self, other): if not isinstance(other, type(self)): return False return self.name == other.name def accept(self, visitor): return visitor.visitLazyVariable(self)
[docs] def eval(self, data_mask, eval_env): """Evaluates variable. First it looks for the variable in ``data_mask``. If not found there, it looks in ``eval_env``. Then it just returns the value the variable represents in either the data mask or the evaluation environment. Parameters ---------- data_mask: pd.DataFrame The data frame where variables are taken from eval_env: EvalEnvironment The environment where values and functions are taken from. Returns ------- result: The value represented by this name in either the data mask or the environment. """ try: result = data_mask[self.name] except KeyError: try: result = eval_env.namespace[self.name] except KeyError as e: raise e return result
[docs]class LazyValue: """Lazy representation of a value in Python. This object holds a value (a string or a number). It returns its value only when it is evaluated via ``.eval()``. Parameters ---------- value: string or numeric The value it holds. """ def __init__(self, value): self.value = value def __str__(self): return str(self.value) def __repr__(self): return self.__str__() def __eq__(self, other): if not isinstance(other, type(self)): return False return self.value == other.value def __hash__(self): return hash((self.value)) def accept(self, visitor): return visitor.visitLazyValue(self)
[docs] def eval(self, data_mask, eval_env): # pylint: disable = unused-argument """Evaluates the value. Simply returns the value. Arguments are ignored but required for consistency among all the lazy objects. Parameters ---------- data_mask: pd.DataFrame The data frame where variables are taken from eval_env: EvalEnvironment The environment where values and functions are taken from. Returns ------- value: The value this obejct represents. """ return self.value
[docs]class LazyCall: """Lazy representation of a function call. This class represents a function that can be a stateful transform (a function with memory) whose arguments can also be stateful transforms. To evaluate these functions we don't create a string representing Python code and let ``eval()`` run it. We take care of all the steps of the evaluation to make sure all the possibly nested stateful transformations are handled correctly. Parameters ---------- callee: string The name of the function args: list A list of lazy objects that are evaluated when calling the function this object represents. kwargs: dict A dictionary of named arguments that are evaluated when calling the function this object represents. """ def __init__(self, callee, args, kwargs): self.callee = callee self.args = args self.kwargs = kwargs self.stateful_transform = None if self.callee in STATEFUL_TRANSFORMS: self.stateful_transform = STATEFUL_TRANSFORMS[self.callee]() def __str__(self): args = [str(arg) for arg in self.args] kwargs = [f"{name} = {str(arg)}" for name, arg in self.kwargs.items()] return f"{self.callee}({', '.join(args + kwargs)})" def __repr__(self): return self.__str__() def __hash__(self): return hash((self.callee, *self.args, *self.kwargs)) def __eq__(self, other): return ( self.callee == other.callee and set(self.args) == set(other.args) and set(self.kwargs) == set(other.kwargs) ) def accept(self, visitor): return visitor.visitLazyCall(self)
[docs] def eval(self, data_mask, eval_env): """Evaluate the call. This method first evaluates all its arguments, which are themselves lazy objects, and then proceeds to evaluate the call it represents. Parameters ---------- data_mask: pd.DataFrame The data frame where variables are taken from eval_env: EvalEnvironment The environment where values and functions are taken from. Returns ------- result: The result of the call evaluation. """ if self.stateful_transform: callee = self.stateful_transform else: callee = eval_env.eval(self.callee) args = [arg.eval(data_mask, eval_env) for arg in self.args] kwargs = {name: arg.eval(data_mask, eval_env) for name, arg in self.kwargs.items()} return callee(*args, **kwargs)
class CallResolver: """Visitor that walks an AST representing a regular call and returns a lazy version of it.""" def __init__(self, expr): self.expr = expr def resolve(self): return self.expr.accept(self) def visitGroupingExpr(self, expr): return expr.expression.accept(self) def visitBinaryExpr(self, expr): otype = expr.operator.type if otype == "PLUS": op = operator.add elif otype == "MINUS": op = operator.sub elif otype == "STAR_STAR": op = operator.pow elif otype == "STAR": op = operator.mul elif otype == "SLASH": op = operator.truediv else: raise CallResolverError(f"Can't resolve call wih binary expression of type '{otype}'") return LazyOperator(op, expr.left.accept(self), expr.right.accept(self)) def visitUnaryExpr(self, expr): otype = expr.operator.type if otype == "PLUS": op = operator.pos elif otype == "MINUS": op = operator.neg else: raise CallResolverError(f"Can't resolve unary expression of type '{otype}'") return LazyOperator(op, expr.right.accept(self)) def visitCallExpr(self, expr): args = [] kwargs = {} for arg in expr.args: if isinstance(arg, Assign): kwargs[arg.name.name.lexeme] = arg.value.accept(self) else: args.append(arg.accept(self)) return LazyCall(expr.callee.name.lexeme, args, kwargs) def visitVariableExpr(self, expr): return LazyVariable(expr.name.lexeme) def visitLiteralExpr(self, expr): return LazyValue(expr.value) def visitQuotedNameExpr(self, expr): return LazyVariable(expr.expression.lexeme[1:-1])