From f8685702bb525bdf161482b9c33a75b020e685fb Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 16 May 2024 08:07:54 -0400 Subject: [PATCH 1/5] feat: faster expression evaluation --- src/app_model/expressions/_expressions.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/app_model/expressions/_expressions.py b/src/app_model/expressions/_expressions.py index fdb8d7d0..7ced0ac2 100644 --- a/src/app_model/expressions/_expressions.py +++ b/src/app_model/expressions/_expressions.py @@ -177,16 +177,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 +364,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. From bb3973f1c1dcc4df5bdb27f1abbc0bea4f30378e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 16 May 2024 08:28:22 -0400 Subject: [PATCH 2/5] feat: faster expression evaluation --- src/app_model/expressions/_expressions.py | 3 +++ tests/test_context/test_expressions.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app_model/expressions/_expressions.py b/src/app_model/expressions/_expressions.py index 7ced0ac2..af888461 100644 --- a/src/app_model/expressions/_expressions.py +++ b/src/app_model/expressions/_expressions.py @@ -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' diff --git a/tests/test_context/test_expressions.py b/tests/test_context/test_expressions.py index a45d0587..71d536df 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 From f868d3b766da544b975108ab11c095146f52cecf Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 17 May 2024 07:46:57 -0400 Subject: [PATCH 3/5] test: coverage --- tests/test_context/test_expressions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_context/test_expressions.py b/tests/test_context/test_expressions.py index 71d536df..895bcdda 100644 --- a/tests/test_context/test_expressions.py +++ b/tests/test_context/test_expressions.py @@ -236,6 +236,11 @@ 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 + + @pytest.mark.parametrize("expr", GOOD_EXPRESSIONS) def test_hash(expr): assert isinstance(hash(parse_expression(expr)), int) From e0d4eb5591c19f3946aa61099641fab3f924d70e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 17 May 2024 07:47:12 -0400 Subject: [PATCH 4/5] remove unnecessary --- src/app_model/expressions/_expressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app_model/expressions/_expressions.py b/src/app_model/expressions/_expressions.py index af888461..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]): From c2df7a235c25cbb6f21a7bd60c905243de300a8b Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 17 May 2024 08:10:26 -0400 Subject: [PATCH 5/5] test cov --- tests/test_context/test_expressions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_context/test_expressions.py b/tests/test_context/test_expressions.py index 895bcdda..a62d69fb 100644 --- a/tests/test_context/test_expressions.py +++ b/tests/test_context/test_expressions.py @@ -239,6 +239,7 @@ def test_safe_eval(): 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)