Skip to content

Commit aa34fd0

Browse files
authored
fix: make filters hashable again (#2556)
1 parent 1c5b6a0 commit aa34fd0

4 files changed

Lines changed: 51 additions & 10 deletions

File tree

cognite/client/data_classes/data_modeling/query.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,13 @@ class NodeOrEdgeResultSetExpression(ResultSetExpression, ABC):
286286
direction: Literal["outwards", "inwards"] = "outwards"
287287
chain_to: Literal["destination", "source"] = "destination"
288288

289+
def __eq__(self, other: object) -> bool:
290+
if not isinstance(other, NodeOrEdgeResultSetExpression):
291+
return NotImplemented
292+
return type(self) is type(other) and self.dump() == other.dump()
289293

290-
@dataclass
294+
295+
@dataclass(eq=False) # Prevents @dataclass from generating its own __eq__, so the parent's is used
291296
class NodeResultSetExpression(NodeOrEdgeResultSetExpression):
292297
"""Describes how to query for nodes in the data model.
293298
@@ -355,7 +360,7 @@ def dump(self, camel_case: bool = True) -> dict[str, Any]:
355360
return output
356361

357362

358-
@dataclass
363+
@dataclass(eq=False) # Prevents @dataclass from generating its own __eq__, so the parent's is used
359364
class EdgeResultSetExpression(NodeOrEdgeResultSetExpression):
360365
"""Describes how to query for edges in the data model.
361366
@@ -439,6 +444,11 @@ class ResultSetExpressionSync(ResultSetExpressionBase, ABC):
439444
sync_mode: SyncMode | None = None
440445
backfill_sort: list[InstanceSort] = field(default_factory=list)
441446

447+
def __eq__(self, other: object) -> bool:
448+
if not isinstance(other, ResultSetExpressionSync):
449+
return NotImplemented
450+
return type(self) is type(other) and self.dump() == other.dump()
451+
442452
@classmethod
443453
def _load(cls, resource: dict[str, Any]) -> ResultSetExpressionSync:
444454
if "nodes" in resource:
@@ -475,7 +485,7 @@ def _dump_sync_mode(sync_mode: SyncMode, camel_case: bool = True) -> str:
475485
assert_never(sync_mode)
476486

477487

478-
@dataclass
488+
@dataclass(eq=False) # Prevents @dataclass from generating its own __eq__, so the parent's is used
479489
class NodeResultSetExpressionSync(ResultSetExpressionSync):
480490
"""Describes how to query for nodes in the data model.
481491
@@ -562,7 +572,7 @@ def dump(self, camel_case: bool = True) -> dict[str, Any]:
562572
return output
563573

564574

565-
@dataclass
575+
@dataclass(eq=False) # Prevents @dataclass from generating its own __eq__, so the parent's is used
566576
class EdgeResultSetExpressionSync(ResultSetExpressionSync):
567577
"""Describes how to query for edges in the data model.
568578

cognite/client/data_classes/filters.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,6 @@ def __bool__(self) -> bool:
9797
)
9898
return True
9999

100-
def __eq__(self, other: object) -> bool:
101-
if not isinstance(other, Filter):
102-
return NotImplemented
103-
return type(self) is type(other) and self.dump() == other.dump()
104-
105100
def dump(self, camel_case_property: bool = False) -> dict[str, Any]:
106101
"""
107102
Dump the filter to a dictionary.

tests/tests_unit/test_data_classes/test_data_models/test_queries.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,30 @@ def test_dump(self, raw_data: dict, loaded: q.ResultSetExpression) -> None:
142142
assert loaded.dump(camel_case=True) == raw_data
143143

144144

145+
class TestResultSetExpressionsSync:
146+
def test_dump_load_equals(self) -> None:
147+
expr = q.NodeResultSetExpressionSync(
148+
filter=f.Equals(property=["node", "externalId"], value="my-node"),
149+
limit=10,
150+
from_="bla",
151+
)
152+
assert expr == q.ResultSetExpressionSync.load(expr.dump(camel_case=True))
153+
154+
def test_load(self) -> None:
155+
raw = {
156+
"nodes": {
157+
"filter": {"equals": {"property": ["edge", "type"], "value": {"space": "sp", "externalId": "tp"}}},
158+
"chainTo": "destination",
159+
"direction": "outwards",
160+
"skipAlreadyDeleted": True,
161+
}
162+
}
163+
loaded = q.NodeResultSetExpressionSync(
164+
filter=f.Equals(property=["edge", "type"], value={"space": "sp", "externalId": "tp"})
165+
)
166+
assert q.ResultSetExpressionSync.load(raw) == loaded
167+
168+
145169
def select_load_and_dump_equals_data() -> Iterator[ParameterSet]:
146170
raw: dict[str, Any] = {}
147171
loaded = q.Select()

tests/tests_unit/test_data_classes/test_filters.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from cognite.client.data_classes.filters import And, Filter, In, Or
7+
from cognite.client.data_classes.filters import And, Equals, Filter, In, Or
88
from tests.utils import FakeCogniteResourceGenerator
99

1010

@@ -52,3 +52,15 @@ def test_filters_warn_in_boolean_contexts() -> None:
5252
)
5353
def test_filter_property_case_conversion(user_filter: Filter, expected: dict) -> None:
5454
assert user_filter.dump(camel_case_property=True) == expected
55+
56+
57+
def test_filter_is_hashable_and_uses_identity() -> None:
58+
# Bug in 8.0.0 to 8.0.5: __eq__ was added to Filter thus implicitly setting __hash__ = None,
59+
# making filters unhashable.
60+
flt = Equals(property=["node", "type"], value="pump")
61+
hash(flt) # must not raise
62+
63+
# Hashing is identity-based: two filters with equal content hash differently.
64+
flt2 = Equals(property=["node", "type"], value="pump")
65+
assert hash(flt) != hash(flt2)
66+
assert flt != flt2

0 commit comments

Comments
 (0)