Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions src/app_model/expressions/_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down Expand Up @@ -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'
Expand All @@ -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), "<Expr>", "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), "<Expr>", "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
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion tests/test_context/test_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)