diff --git a/src/app_model/expressions/_expressions.py b/src/app_model/expressions/_expressions.py index fdb8d7d0..af898a59 100644 --- a/src/app_model/expressions/_expressions.py +++ b/src/app_model/expressions/_expressions.py @@ -86,7 +86,7 @@ def safe_eval(expr: str | bool | Expr, context: Mapping | None = None) -> Any: """ if isinstance(expr, bool): return expr - return parse_expression(expr).eval(context or {}) + return parse_expression(expr).eval(context) class Expr(ast.AST, Generic[T]): @@ -151,6 +151,9 @@ class Expr(ast.AST, Generic[T]): >>> new_expr.eval(dict(v2="hello!", myvar=8)) 'hello!' + you can also use keyword arguments. This is *slightly* slower + >>> new_expr.eval(v2="hello!", myvar=4) + serialize >>> str(new_expr) 'myvar > 5 and v2' @@ -177,16 +180,21 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: raise RuntimeError("Don't instantiate Expr. Use `Expr.parse`") super().__init__(*args, **kwargs) ast.fix_missing_locations(self) + self._code = compile(ast.Expression(body=self), "", "eval") + self._names = set(_iter_names(self)) - def eval(self, context: Mapping[str, object] | None = None) -> T: + def eval( + self, context: Mapping[str, object] | None = None, **ctx_kwargs: object + ) -> T: """Evaluate this expression with names in `context`.""" if context is None: - context = {} - code = compile(ast.Expression(body=self), "", "eval") + context = ctx_kwargs + elif ctx_kwargs: + context = {**context, **ctx_kwargs} try: - return cast(T, eval(code, {}, context)) + return eval(self._code, {}, context) # type: ignore except NameError as e: - miss = {k for k in _iter_names(self) if k not in context} + miss = {k for k in self._names if k not in context} raise NameError( f"Names required to eval this expression are missing: {miss}" ) from e @@ -359,12 +367,6 @@ def __init__(self, id: str, ctx: ast.expr_context = LOAD, **kwargs: Any) -> None kwargs["ctx"] = LOAD super().__init__(id, **kwargs) - def eval(self, context: Mapping | None = None) -> T: - """Evaluate this expression with names in `context`.""" - if context is None: - context = {} - return super().eval(context=context) - class Constant(Expr[V], ast.Constant): """A constant value. diff --git a/tests/test_context/test_expressions.py b/tests/test_context/test_expressions.py index a45d0587..a62d69fb 100644 --- a/tests/test_context/test_expressions.py +++ b/tests/test_context/test_expressions.py @@ -172,7 +172,7 @@ def test_iter_names(): for k, v in _OPS.items(): if issubclass(k, ast.unaryop): GOOD_EXPRESSIONS.append(f"{v} 1" if v == "not" else f"{v}1") - else: + elif v not in {"is", "is not"}: GOOD_EXPRESSIONS.append(f"1 {v} 2") # these are not supported @@ -236,6 +236,12 @@ def test_safe_eval(): safe_eval("{1,2,3}") +def test_eval_kwargs(): + expr = parse_expression("a + b") + assert expr.eval(a=1, b=2) == 3 + assert expr.eval({"a": 2}, b=2) == 4 + + @pytest.mark.parametrize("expr", GOOD_EXPRESSIONS) def test_hash(expr): assert isinstance(hash(parse_expression(expr)), int)