Skip to content

Commit

Permalink
fix: expression hashing and repr (#39)
Browse files Browse the repository at this point in the history
* fix: expression hashing and repr

* test: add tests
  • Loading branch information
tlambert03 authored Jul 14, 2022
1 parent 9923ced commit b3f1a10
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 14 deletions.
24 changes: 17 additions & 7 deletions src/app_model/expressions/_expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
from __future__ import annotations

import ast
import sys
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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))

Expand Down
5 changes: 4 additions & 1 deletion tests/test_context/test_context_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 9 additions & 6 deletions tests/test_context/test_expressions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import ast
import sys
from copy import deepcopy

import pytest
Expand All @@ -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():
Expand All @@ -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):
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)

0 comments on commit b3f1a10

Please sign in to comment.