Skip to content

Commit abef683

Browse files
committed
Add packstream support for element_id fields
1 parent fad3144 commit abef683

File tree

4 files changed

+475
-83
lines changed

4 files changed

+475
-83
lines changed

neo4j/graph/__init__.py

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131

3232
from collections.abc import Mapping
3333

34+
from ..meta import deprecated
35+
3436

3537
class Graph:
3638
""" Local, self-contained graph object that acts as a container for
@@ -71,35 +73,61 @@ class Hydrator:
7173
def __init__(self, graph):
7274
self.graph = graph
7375

74-
def hydrate_node(self, n_id, n_labels=None, properties=None):
76+
def hydrate_node(self, id_, labels=None,
77+
properties=None, element_id=None):
7578
assert isinstance(self.graph, Graph)
79+
# backwards compatibility with Neo4j < 5.0
80+
if element_id is None:
81+
element_id = str(id_)
82+
7683
try:
77-
inst = self.graph._nodes[n_id]
84+
inst = self.graph._nodes[element_id]
7885
except KeyError:
79-
inst = self.graph._nodes[n_id] = Node(self.graph, n_id, n_labels, properties)
86+
inst = self.graph._nodes[element_id] = Node(
87+
self.graph, element_id, id_, labels, properties
88+
)
8089
else:
8190
# If we have already hydrated this node as the endpoint of
8291
# a relationship, it won't have any labels or properties.
8392
# Therefore, we need to add the ones we have here.
84-
if n_labels:
85-
inst._labels = inst._labels.union(n_labels) # frozen_set
93+
if labels:
94+
inst._labels = inst._labels.union(labels) # frozen_set
8695
if properties:
8796
inst._properties.update(properties)
8897
return inst
8998

90-
def hydrate_relationship(self, r_id, n0_id, n1_id, r_type, properties=None):
91-
inst = self.hydrate_unbound_relationship(r_id, r_type, properties)
92-
inst._start_node = self.hydrate_node(n0_id)
93-
inst._end_node = self.hydrate_node(n1_id)
99+
def hydrate_relationship(self, id_, n0_id, n1_id, type_,
100+
properties=None, element_id=None,
101+
n0_element_id=None, n1_element_id=None):
102+
# backwards compatibility with Neo4j < 5.0
103+
if element_id is None:
104+
element_id = str(id_)
105+
if n0_element_id is None:
106+
n0_element_id = str(n0_id)
107+
if n1_element_id is None:
108+
n1_element_id = str(n1_id)
109+
110+
inst = self.hydrate_unbound_relationship(id_, type_, properties,
111+
element_id)
112+
inst._start_node = self.hydrate_node(n0_id,
113+
element_id=n0_element_id)
114+
inst._end_node = self.hydrate_node(n1_id, element_id=n1_element_id)
94115
return inst
95116

96-
def hydrate_unbound_relationship(self, r_id, r_type, properties=None):
117+
def hydrate_unbound_relationship(self, id_, type_, properties=None,
118+
element_id=None):
97119
assert isinstance(self.graph, Graph)
120+
# backwards compatibility with Neo4j < 5.0
121+
if element_id is None:
122+
element_id = str(id_)
123+
98124
try:
99-
inst = self.graph._relationships[r_id]
125+
inst = self.graph._relationships[element_id]
100126
except KeyError:
101-
r = self.graph.relationship_type(r_type)
102-
inst = self.graph._relationships[r_id] = r(self.graph, r_id, properties)
127+
r = self.graph.relationship_type(type_)
128+
inst = self.graph._relationships[element_id] = r(
129+
self.graph, element_id, id_, properties
130+
)
103131
return inst
104132

105133
def hydrate_path(self, nodes, relationships, sequence):
@@ -131,22 +159,27 @@ class Entity(Mapping):
131159
functionality.
132160
"""
133161

134-
def __init__(self, graph, id, properties):
162+
def __init__(self, graph, element_id, id_, properties):
135163
self._graph = graph
136-
self._id = id
137-
self._properties = dict((k, v) for k, v in (properties or {}).items() if v is not None)
164+
self._element_id = element_id
165+
self._id = id_
166+
self._properties = {
167+
k: v for k, v in (properties or {}).items() if v is not None
168+
}
138169

139170
def __eq__(self, other):
140171
try:
141-
return type(self) == type(other) and self.graph == other.graph and self.id == other.id
172+
return (type(self) == type(other)
173+
and self.graph == other.graph
174+
and self.element_id == other.element_id)
142175
except AttributeError:
143176
return False
144177

145178
def __ne__(self, other):
146179
return not self.__eq__(other)
147180

148181
def __hash__(self):
149-
return hash(self.id)
182+
return hash(self._element_id)
150183

151184
def __len__(self):
152185
return len(self._properties)
@@ -167,11 +200,30 @@ def graph(self):
167200
return self._graph
168201

169202
@property
203+
@deprecated("`id` is deprecated, use `element_id` instead")
170204
def id(self):
171-
""" The identity of this entity in its container :class:`.Graph`.
205+
"""The legacy identity of this entity in its container :class:`.Graph`.
206+
207+
Depending on the version of the server this entity was retrieved from,
208+
this may be empty (None).
209+
210+
.. deprecated:: 5.0
211+
Use :attr:`.element_id` instead.
212+
213+
:rtype: int
172214
"""
173215
return self._id
174216

217+
@property
218+
def element_id(self):
219+
"""The identity of this entity in its container :class:`.Graph`.
220+
221+
.. added:: 5.0
222+
223+
:rtype: str
224+
"""
225+
return self._element_id
226+
175227
def get(self, name, default=None):
176228
""" Get a property value by name, optionally with a default.
177229
"""
@@ -214,12 +266,14 @@ class Node(Entity):
214266
""" Self-contained graph node.
215267
"""
216268

217-
def __init__(self, graph, n_id, n_labels=None, properties=None):
218-
Entity.__init__(self, graph, n_id, properties)
269+
def __init__(self, graph, element_id, id_, n_labels=None,
270+
properties=None):
271+
Entity.__init__(self, graph, element_id, id_, properties)
219272
self._labels = frozenset(n_labels or ())
220273

221274
def __repr__(self):
222-
return "<Node id=%r labels=%r properties=%r>" % (self._id, self._labels, self._properties)
275+
return (f"<Node element_id={self._element_id!r} "
276+
f"labels={self._labels!r} properties={self._properties!r}>")
223277

224278
@property
225279
def labels(self):
@@ -232,14 +286,15 @@ class Relationship(Entity):
232286
""" Self-contained graph relationship.
233287
"""
234288

235-
def __init__(self, graph, r_id, properties):
236-
Entity.__init__(self, graph, r_id, properties)
289+
def __init__(self, graph, element_id, id_, properties):
290+
Entity.__init__(self, graph, element_id, id_, properties)
237291
self._start_node = None
238292
self._end_node = None
239293

240294
def __repr__(self):
241-
return "<Relationship id=%r nodes=(%r, %r) type=%r properties=%r>" % (
242-
self._id, self._start_node, self._end_node, self.type, self._properties)
295+
return (f"<Relationship element_id={self._element_id!r} "
296+
f"nodes={self.nodes!r} type={self.type!r} "
297+
f"properties={self._properties!r}>")
243298

244299
@property
245300
def nodes(self):

tests/unit/common/test_data.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,96 @@
1616
# limitations under the License.
1717

1818

19+
import pytest
20+
1921
from neo4j.data import DataHydrator
2022
from neo4j.packstream import Structure
2123

2224

2325
# python -m pytest -s -v tests/unit/test_data.py
2426

2527

26-
def test_can_hydrate_node_structure():
28+
def test_can_hydrate_v1_node_structure():
2729
hydrant = DataHydrator()
2830

2931
struct = Structure(b'N', 123, ["Person"], {"name": "Alice"})
3032
alice, = hydrant.hydrate([struct])
3133

32-
assert alice.id == 123
34+
with pytest.warns(DeprecationWarning, match="element_id"):
35+
assert alice.id == 123
36+
# for backwards compatibility, the driver should compy the element_id
37+
assert alice.element_id == "123"
38+
assert alice.labels == {"Person"}
39+
assert set(alice.keys()) == {"name"}
40+
assert alice.get("name") == "Alice"
41+
42+
43+
@pytest.mark.parametrize("with_id", (True, False))
44+
def test_can_hydrate_v2_node_structure(with_id):
45+
hydrant = DataHydrator()
46+
47+
id_ = 123 if with_id else None
48+
49+
struct = Structure(b'N', id_, ["Person"], {"name": "Alice"}, "abc")
50+
alice, = hydrant.hydrate([struct])
51+
52+
with pytest.warns(DeprecationWarning, match="element_id"):
53+
assert alice.id == id_
54+
assert alice.element_id == "abc"
3355
assert alice.labels == {"Person"}
3456
assert set(alice.keys()) == {"name"}
3557
assert alice.get("name") == "Alice"
3658

3759

60+
def test_can_hydrate_v1_relationship_structure():
61+
hydrant = DataHydrator()
62+
63+
struct = Structure(b'R', 123, 456, 789, "KNOWS", {"since": 1999})
64+
rel, = hydrant.hydrate([struct])
65+
66+
with pytest.warns(DeprecationWarning, match="element_id"):
67+
assert rel.id == 123
68+
with pytest.warns(DeprecationWarning, match="element_id"):
69+
assert rel.start_node.id == 456
70+
with pytest.warns(DeprecationWarning, match="element_id"):
71+
assert rel.end_node.id == 789
72+
# for backwards compatibility, the driver should compy the element_id
73+
assert rel.element_id == "123"
74+
assert rel.start_node.element_id == "456"
75+
assert rel.end_node.element_id == "789"
76+
assert rel.type == "KNOWS"
77+
assert set(rel.keys()) == {"since"}
78+
assert rel.get("since") == 1999
79+
80+
81+
@pytest.mark.parametrize("with_ids", (True, False))
82+
def test_can_hydrate_v2_relationship_structure(with_ids):
83+
hydrant = DataHydrator()
84+
85+
id_ = 123 if with_ids else None
86+
start_id = 456 if with_ids else None
87+
end_id = 789 if with_ids else None
88+
89+
struct = Structure(b'R', id_, start_id, end_id, "KNOWS", {"since": 1999},
90+
"abc", "def", "ghi")
91+
92+
rel, = hydrant.hydrate([struct])
93+
94+
with pytest.warns(DeprecationWarning, match="element_id"):
95+
assert rel.id == id_
96+
with pytest.warns(DeprecationWarning, match="element_id"):
97+
assert rel.start_node.id == start_id
98+
with pytest.warns(DeprecationWarning, match="element_id"):
99+
assert rel.end_node.id == end_id
100+
# for backwards compatibility, the driver should compy the element_id
101+
assert rel.element_id == "abc"
102+
assert rel.start_node.element_id == "def"
103+
assert rel.end_node.element_id == "ghi"
104+
assert rel.type == "KNOWS"
105+
assert set(rel.keys()) == {"since"}
106+
assert rel.get("since") == 1999
107+
108+
38109
def test_hydrating_unknown_structure_returns_same():
39110
hydrant = DataHydrator()
40111

tests/unit/common/test_record.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def test_record_repr(len_):
273273
{"x": {"one": 1, "two": 2}}
274274
),
275275
(
276-
zip(["a"], [Node("graph", 42, "Person", {"name": "Alice"})]),
276+
zip(["a"], [Node("graph", "42", 42, "Person", {"name": "Alice"})]),
277277
(),
278278
{"a": {"name": "Alice"}}
279279
),

0 commit comments

Comments
 (0)