Skip to content

Commit bb028d4

Browse files
authored
Merge pull request #345 from technige/4.0-data-method
Improved Result.data method for graphy types
2 parents 3024ac2 + 7505cad commit bb028d4

File tree

4 files changed

+194
-57
lines changed

4 files changed

+194
-57
lines changed

neo4j/data.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
# limitations under the License.
2020

2121

22-
from collections.abc import Mapping, Sequence
22+
from abc import ABCMeta, abstractmethod
23+
from collections.abc import Sequence, Set, Mapping
2324
from datetime import date, time, datetime, timedelta
2425
from functools import reduce
2526
from operator import xor as xor_operator
2627

2728
from neo4j.conf import iter_items
28-
from neo4j.graph import Graph
29+
from neo4j.graph import Graph, Node, Relationship, Path
2930
from neo4j.packstream import INT64_MIN, INT64_MAX, Structure
3031
from neo4j.spatial import Point, hydrate_point, dehydrate_point
3132
from neo4j.time import Date, Time, DateTime, Duration
@@ -212,20 +213,57 @@ def data(self, *keys):
212213
:return: dictionary of values, keyed by field name
213214
:raises: :exc:`IndexError` if an out-of-bounds index is specified
214215
"""
215-
if keys:
216-
d = {}
217-
for key in keys:
218-
try:
219-
i = self.index(key)
220-
except KeyError:
221-
d[key] = None
222-
else:
223-
d[self.__keys[i]] = self[i]
224-
return d
225-
return dict(self)
216+
return RecordExporter().transform(dict(self.items(*keys)))
217+
218+
219+
class DataTransformer(metaclass=ABCMeta):
220+
""" Abstract base class for transforming data from one form into
221+
another.
222+
"""
223+
224+
@abstractmethod
225+
def transform(self, x):
226+
""" Transform a value, or collection of values.
227+
228+
:param x: input value
229+
:return: output value
230+
"""
231+
232+
233+
class RecordExporter(DataTransformer):
234+
""" Transformer class used by the :meth:`.Record.data` method.
235+
"""
236+
237+
def transform(self, x):
238+
if isinstance(x, Node):
239+
return self.transform(dict(x))
240+
elif isinstance(x, Relationship):
241+
return (self.transform(dict(x.start_node)),
242+
x.__class__.__name__,
243+
self.transform(dict(x.end_node)))
244+
elif isinstance(x, Path):
245+
path = [self.transform(x.start_node)]
246+
for i, relationship in enumerate(x.relationships):
247+
path.append(self.transform(relationship.__class__.__name__))
248+
path.append(self.transform(x.nodes[i + 1]))
249+
return path
250+
elif isinstance(x, str):
251+
return x
252+
elif isinstance(x, Sequence):
253+
t = type(x)
254+
return t(map(self.transform, x))
255+
elif isinstance(x, Set):
256+
t = type(x)
257+
return t(map(self.transform, x))
258+
elif isinstance(x, Mapping):
259+
t = type(x)
260+
return t((k, self.transform(v)) for k, v in x.items())
261+
else:
262+
return x
226263

227264

228265
class DataHydrator:
266+
# TODO: extend DataTransformer
229267

230268
def __init__(self):
231269
super(DataHydrator, self).__init__()
@@ -276,6 +314,7 @@ def hydrate_records(self, keys, record_values):
276314

277315

278316
class DataDehydrator:
317+
# TODO: extend DataTransformer
279318

280319
@classmethod
281320
def fix_parameters(cls, parameters):

neo4j/graph/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ def hydrate_node(self, n_id, n_labels=None, properties=None):
7979
inst = self.graph._nodes[n_id]
8080
except KeyError:
8181
inst = self.graph._nodes[n_id] = Node(self.graph, n_id, n_labels, properties)
82+
else:
83+
# If we have already hydrated this node as the endpoint of
84+
# a relationship, it won't have any labels or properties.
85+
# Therefore, we need to add the ones we have here.
86+
if n_labels:
87+
inst._labels = frozenset(inst._labels & set(n_labels))
88+
if properties:
89+
inst._properties.update(properties)
8290
return inst
8391

8492
def hydrate_relationship(self, r_id, n0_id, n1_id, r_type, properties=None):

tests/integration/test_result.py

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -208,30 +208,6 @@ def test_multiple_keyed_values(session):
208208
[9, 3]]
209209

210210

211-
def test_multiple_data(session):
212-
result = session.run("UNWIND range(1, 3) AS n "
213-
"RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
214-
assert result.data() == [{"x": 1, "y": 2, "z": 3},
215-
{"x": 2, "y": 4, "z": 6},
216-
{"x": 3, "y": 6, "z": 9}]
217-
218-
219-
def test_multiple_indexed_data(session):
220-
result = session.run("UNWIND range(1, 3) AS n "
221-
"RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
222-
assert result.data(2, 0) == [{"x": 1, "z": 3},
223-
{"x": 2, "z": 6},
224-
{"x": 3, "z": 9}]
225-
226-
227-
def test_multiple_keyed_data(session):
228-
result = session.run("UNWIND range(1, 3) AS n "
229-
"RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
230-
assert result.data("z", "x") == [{"x": 1, "z": 3},
231-
{"x": 2, "z": 6},
232-
{"x": 3, "z": 9}]
233-
234-
235211
def test_value_with_no_keys_and_no_records(neo4j_driver):
236212
with neo4j_driver.session() as session:
237213
result = session.run("CREATE ()")
@@ -243,11 +219,6 @@ def test_values_with_one_key_and_no_records(session):
243219
assert result.values() == []
244220

245221

246-
def test_data_with_one_key_and_no_records(session):
247-
result = session.run("UNWIND range(1, 0) AS n RETURN n")
248-
assert result.data() == []
249-
250-
251222
def test_single_with_no_keys_and_no_records(session):
252223
result = session.run("CREATE ()")
253224
record = result.single()
@@ -311,18 +282,3 @@ def test_single_indexed_values(session):
311282
def test_single_keyed_values(session):
312283
result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
313284
assert result.single().values("z", "x") == [3, 1]
314-
315-
316-
def test_single_data(session):
317-
result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
318-
assert result.single().data() == {"x": 1, "y": 2, "z": 3}
319-
320-
321-
def test_single_indexed_data(session):
322-
result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
323-
assert result.single().data(2, 0) == {"x": 1, "z": 3}
324-
325-
326-
def test_single_keyed_data(session):
327-
result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
328-
assert result.single().data("z", "x") == {"x": 1, "z": 3}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env python
2+
# -*- encoding: utf-8 -*-
3+
4+
# Copyright (c) 2002-2020 "Neo4j,"
5+
# Neo4j Sweden AB [http://neo4j.com]
6+
#
7+
# This file is part of Neo4j.
8+
#
9+
# Licensed under the Apache License, Version 2.0 (the "License");
10+
# you may not use this file except in compliance with the License.
11+
# You may obtain a copy of the License at
12+
#
13+
# http://www.apache.org/licenses/LICENSE-2.0
14+
#
15+
# Unless required by applicable law or agreed to in writing, software
16+
# distributed under the License is distributed on an "AS IS" BASIS,
17+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
# See the License for the specific language governing permissions and
19+
# limitations under the License.
20+
21+
22+
def test_data_with_one_key_and_no_records(session):
23+
result = session.run("UNWIND range(1, 0) AS n RETURN n")
24+
assert result.data() == []
25+
26+
27+
def test_multiple_data(session):
28+
result = session.run("UNWIND range(1, 3) AS n "
29+
"RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
30+
assert result.data() == [{"x": 1, "y": 2, "z": 3},
31+
{"x": 2, "y": 4, "z": 6},
32+
{"x": 3, "y": 6, "z": 9}]
33+
34+
35+
def test_multiple_indexed_data(session):
36+
result = session.run("UNWIND range(1, 3) AS n "
37+
"RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
38+
assert result.data(2, 0) == [{"x": 1, "z": 3},
39+
{"x": 2, "z": 6},
40+
{"x": 3, "z": 9}]
41+
42+
43+
def test_multiple_keyed_data(session):
44+
result = session.run("UNWIND range(1, 3) AS n "
45+
"RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
46+
assert result.data("z", "x") == [{"x": 1, "z": 3},
47+
{"x": 2, "z": 6},
48+
{"x": 3, "z": 9}]
49+
50+
51+
def test_single_data(session):
52+
result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
53+
assert result.single().data() == {"x": 1, "y": 2, "z": 3}
54+
55+
56+
def test_single_indexed_data(session):
57+
result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
58+
assert result.single().data(2, 0) == {"x": 1, "z": 3}
59+
60+
61+
def test_single_keyed_data(session):
62+
result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
63+
assert result.single().data("z", "x") == {"x": 1, "z": 3}
64+
65+
66+
def test_none(session):
67+
result = session.run("RETURN null AS x")
68+
assert result.data() == [{"x": None}]
69+
70+
71+
def test_bool(session):
72+
result = session.run("RETURN true AS x, false AS y")
73+
assert result.data() == [{"x": True, "y": False}]
74+
75+
76+
def test_int(session):
77+
result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
78+
assert result.data() == [{"x": 1, "y": 2, "z": 3}]
79+
80+
81+
def test_float(session):
82+
result = session.run("RETURN 0.0 AS x, 1.0 AS y, 3.141592653589 AS z")
83+
assert result.data() == [{"x": 0.0, "y": 1.0, "z": 3.141592653589}]
84+
85+
86+
def test_string(session):
87+
result = session.run("RETURN 'hello, world' AS x")
88+
assert result.data() == [{"x": "hello, world"}]
89+
90+
91+
def test_byte_array(session):
92+
result = session.run("RETURN $x AS x", x=bytearray([1, 2, 3]))
93+
assert result.data() == [{"x": bytearray([1, 2, 3])}]
94+
95+
96+
def test_list(session):
97+
result = session.run("RETURN [1, 2, 3] AS x")
98+
assert result.data() == [{"x": [1, 2, 3]}]
99+
100+
101+
def test_dict(session):
102+
result = session.run("RETURN {one: 1, two: 2} AS x")
103+
assert result.data() == [{"x": {"one": 1, "two": 2}}]
104+
105+
106+
def test_node(session):
107+
result = session.run("CREATE (x:Person {name:'Alice'}) RETURN x")
108+
assert result.data() == [{"x": {"name": "Alice"}}]
109+
110+
111+
def test_relationship_with_pre_known_nodes(session):
112+
result = session.run("CREATE (a:Person {name:'Alice'})-[x:KNOWS {since:1999}]->(b:Person {name:'Bob'}) "
113+
"RETURN a, b, x")
114+
assert result.data() == [{"a": {"name": "Alice"}, "b": {"name": "Bob"},
115+
"x": ({"name": "Alice"}, "KNOWS", {"name": "Bob"})}]
116+
117+
118+
def test_relationship_with_post_known_nodes(session):
119+
result = session.run("CREATE (a:Person {name:'Alice'})-[x:KNOWS {since:1999}]->(b:Person {name:'Bob'}) "
120+
"RETURN x, a, b")
121+
assert result.data() == [{"x": ({"name": "Alice"}, "KNOWS", {"name": "Bob"}),
122+
"a": {"name": "Alice"}, "b": {"name": "Bob"}}]
123+
124+
125+
def test_relationship_with_unknown_nodes(session):
126+
result = session.run("CREATE (:Person {name:'Alice'})-[x:KNOWS {since:1999}]->(:Person {name:'Bob'}) "
127+
"RETURN x")
128+
assert result.data() == [{"x": ({}, "KNOWS", {})}]
129+
130+
131+
def test_path(session):
132+
result = session.run("CREATE x = (a:Person {name:'Alice'})-[:KNOWS {since:1999}]->(b:Person {name:'Bob'}) "
133+
"RETURN x")
134+
assert result.data() == [{"x": [{"name": "Alice"}, "KNOWS", {"name": "Bob"}]}]

0 commit comments

Comments
 (0)