diff --git a/src/app_model/expressions/_expressions.py b/src/app_model/expressions/_expressions.py index 8b6b65c..f46d7ea 100644 --- a/src/app_model/expressions/_expressions.py +++ b/src/app_model/expressions/_expressions.py @@ -26,8 +26,8 @@ from __future__ import annotations import ast -import sys from typing import ( + TYPE_CHECKING, Any, Callable, Dict, @@ -55,6 +55,9 @@ T2 = TypeVar("T2", bound=Union[ConstType, "Expr"]) V = TypeVar("V", bound=ConstType) +if TYPE_CHECKING: + from ._context_keys import ContextKey + def parse_expression(expr: str) -> Expr: """Parse string expression into an :class:`Expr` instance. @@ -190,9 +193,7 @@ def _serialize(self) -> str: return str(_ExprSerializer(self)) def __repr__(self) -> str: - if sys.version_info >= (3, 9): - return ast.dump(self, indent=2) - return ast.dump(self) + return f"Expr.parse({str(self)!r})" @staticmethod def _cast(obj: Any) -> Expr: @@ -313,9 +314,15 @@ def validate(cls, v: Any) -> Expr: return v if isinstance(v, Expr) else parse_expression(v) def __hash__(self) -> int: - return hash(self.__class__) + hash( - tuple(getattr(self, f) for f in self._fields) - ) + _hash = hash(self.__class__) + for f in self._fields: + field = getattr(self, f) + if isinstance(field, list): + field = tuple(field) + if isinstance(field, set): + field = frozenset(field) + _hash += hash(field) + return _hash LOAD = ast.Load() @@ -528,6 +535,9 @@ def __str__(self) -> str: def visit_Name(self, node: ast.Name) -> None: self.write(node.id) + def visit_ContextKey(self, node: ContextKey) -> None: + return self.visit_Name(node) + def visit_Constant(self, node: ast.Constant) -> None: self.write(repr(node.value)) diff --git a/tests/test_context/test_context_keys.py b/tests/test_context/test_context_keys.py index 420d757..0e2c152 100644 --- a/tests/test_context/test_context_keys.py +++ b/tests/test_context/test_context_keys.py @@ -8,12 +8,15 @@ def test_context_key_info(): - ContextKey("default", "description", None, id="some_key") + key = ContextKey("default", "description", None, id="some_key") info = ContextKey.info() assert isinstance(info, list) and len(info) assert all(isinstance(x, ContextKeyInfo) for x in info) assert "some_key" in {x.key for x in info} + assert repr(key) == "Expr.parse('some_key')" + assert repr(key == 1) == "Expr.parse('some_key == 1')" + def _adder(x: list) -> int: return sum(x) diff --git a/tests/test_context/test_expressions.py b/tests/test_context/test_expressions.py index fcec543..29d6ed2 100644 --- a/tests/test_context/test_expressions.py +++ b/tests/test_context/test_expressions.py @@ -1,5 +1,4 @@ import ast -import sys from copy import deepcopy import pytest @@ -15,7 +14,7 @@ def test_names(): with pytest.raises(NameError): Name("n").eval() - assert repr(Name("n")) == "Name(id='n', ctx=Load())" + assert repr(Name("n")) == "Expr.parse('n')" def test_constants(): @@ -33,10 +32,7 @@ def test_constants(): assert Constant(False).eval() is False assert Constant(None).eval() is None - if sys.version_info >= (3, 9): - assert repr(Constant(1)) == "Constant(value=1)" - else: - assert repr(Constant(1)) == "Constant(value=1, kind=None)" + assert repr(Constant(1)) == "Expr.parse('1')" # only {None, str, bytes, bool, int, float} allowed with pytest.raises(TypeError): @@ -140,6 +136,8 @@ def test_comparison(): assert Expr.not_in(Constant("a"), Constant("abcd")).eval() is False assert Constant("a").not_in(Constant("abcd")).eval() is False + assert repr(n > n2) == "Expr.parse('n > n2')" + def test_iter_names(): expr = "a if b in c else d > e" @@ -232,3 +230,8 @@ def test_safe_eval(): with pytest.raises(SyntaxError, match="Type 'Set' not supported"): safe_eval("{1,2,3}") + + +@pytest.mark.parametrize("expr", GOOD_EXPRESSIONS) +def test_hash(expr): + assert isinstance(hash(expr), int)