Skip to content

Commit a4d4d66

Browse files
committed
Add packstream support for element_id fields
1 parent 3bdd5c5 commit a4d4d66

File tree

2 files changed

+150
-26
lines changed

2 files changed

+150
-26
lines changed

neo4j/graph/__init__.py

Lines changed: 77 additions & 24 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,10 +159,13 @@ 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:
@@ -167,11 +198,30 @@ def graph(self):
167198
return self._graph
168199

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

215+
@property
216+
def element_id(self):
217+
"""The identity of this entity in its container :class:`.Graph`.
218+
219+
.. added:: 5.0
220+
221+
:rtype: str
222+
"""
223+
return self._element_id
224+
175225
def get(self, name, default=None):
176226
""" Get a property value by name, optionally with a default.
177227
"""
@@ -214,12 +264,14 @@ class Node(Entity):
214264
""" Self-contained graph node.
215265
"""
216266

217-
def __init__(self, graph, n_id, n_labels=None, properties=None):
218-
Entity.__init__(self, graph, n_id, properties)
267+
def __init__(self, graph, element_id, id_, n_labels=None,
268+
properties=None):
269+
Entity.__init__(self, graph, element_id, id_, properties)
219270
self._labels = frozenset(n_labels or ())
220271

221272
def __repr__(self):
222-
return "<Node id=%r labels=%r properties=%r>" % (self._id, self._labels, self._properties)
273+
return (f"<Node element_id={self._element_id!r} "
274+
f"labels={self._labels!r} properties={self._properties!r}>")
223275

224276
@property
225277
def labels(self):
@@ -232,14 +284,15 @@ class Relationship(Entity):
232284
""" Self-contained graph relationship.
233285
"""
234286

235-
def __init__(self, graph, r_id, properties):
236-
Entity.__init__(self, graph, r_id, properties)
287+
def __init__(self, graph, element_id, id_, properties):
288+
Entity.__init__(self, graph, element_id, id_, properties)
237289
self._start_node = None
238290
self._end_node = None
239291

240292
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)
293+
return (f"<Relationship element_id={self._element_id!r} "
294+
f"nodes={self.nodes!r} type={self.type!r} "
295+
f"properties={self._properties!r}>")
243296

244297
@property
245298
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

0 commit comments

Comments
 (0)