Skip to content

Commit 7a49f31

Browse files
authored
Merge pull request #123 from neo4j/protect-some-neo4j-props
Fix issue of conflicting keys when importing from Neo4j
2 parents 48d3c89 + ba33824 commit 7a49f31

File tree

2 files changed

+110
-16
lines changed

2 files changed

+110
-16
lines changed

python-wrapper/src/neo4j_viz/neo4j.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

3-
from typing import Optional, Union
3+
from collections.abc import Iterable
4+
from typing import Any, Optional, Union
45

56
import neo4j.graph
67
from neo4j import Result
@@ -20,6 +21,9 @@ def from_neo4j(
2021
"""
2122
Create a VisualizationGraph from a Neo4j Graph or Neo4j Result object.
2223
24+
All node and relationship properties will be included in the visualization graph.
25+
If the property names are conflicting with those of `Node` and `Relationship` objects, they will be prefixed
26+
with `__`.
2327
2428
Parameters
2529
----------
@@ -59,12 +63,13 @@ def from_neo4j(
5963

6064

6165
def _map_node(node: neo4j.graph.Node, size_property: Optional[str], caption_property: Optional[str]) -> Node:
66+
labels = sorted([label for label in node.labels])
67+
6268
if size_property:
6369
size = node.get(size_property)
6470
else:
6571
size = None
6672

67-
labels = sorted([label for label in node.labels])
6873
if caption_property:
6974
if caption_property == "labels":
7075
if len(labels) > 0:
@@ -74,7 +79,13 @@ def _map_node(node: neo4j.graph.Node, size_property: Optional[str], caption_prop
7479
else:
7580
caption = str(node.get(caption_property))
7681

77-
return Node(id=node.element_id, caption=caption, labels=labels, size=size, **{k: v for k, v in node.items()})
82+
base_node_props = dict(id=node.element_id, caption=caption, labels=labels, size=size)
83+
84+
protected_props = base_node_props.keys()
85+
additional_node_props = {k: v for k, v in node.items()}
86+
additional_node_props = _rename_protected_props(additional_node_props, protected_props)
87+
88+
return Node(**base_node_props, **additional_node_props)
7889

7990

8091
def _map_relationship(rel: neo4j.graph.Relationship, caption_property: Optional[str]) -> Optional[Relationship]:
@@ -89,11 +100,32 @@ def _map_relationship(rel: neo4j.graph.Relationship, caption_property: Optional[
89100
else:
90101
caption = None
91102

92-
return Relationship(
103+
base_rel_props = dict(
93104
id=rel.element_id,
94105
source=rel.start_node.element_id,
95106
target=rel.end_node.element_id,
96-
type_=rel.type,
107+
_type=rel.type,
97108
caption=caption,
98-
**{k: v for k, v in rel.items()},
99109
)
110+
111+
protected_props = base_rel_props.keys()
112+
additional_rel_props = {k: v for k, v in rel.items()}
113+
additional_rel_props = _rename_protected_props(additional_rel_props, protected_props)
114+
115+
return Relationship(
116+
**base_rel_props,
117+
**additional_rel_props,
118+
)
119+
120+
121+
def _rename_protected_props(
122+
additional_props: dict[str, Any],
123+
protected_props: Iterable[str],
124+
) -> dict[str, Union[str, int, float]]:
125+
for prop in protected_props:
126+
if prop not in additional_props:
127+
continue
128+
129+
additional_props[f"__{prop}"] = additional_props.pop(prop)
130+
131+
return additional_props

python-wrapper/tests/test_neo4j.py

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Generator
22

3+
import neo4j
34
import pytest
45
from neo4j import Session
56

@@ -10,7 +11,8 @@
1011
@pytest.fixture(scope="class", autouse=True)
1112
def graph_setup(neo4j_session: Session) -> Generator[None, None, None]:
1213
neo4j_session.run(
13-
"CREATE (a:_CI_A {name:'Alice', height:20})-[:KNOWS {year: 2025}]->(b:_CI_A:_CI_B {name:'Bob', height:10}), (b)-[:RELATED {year: 2015}]->(a)"
14+
"CREATE (a:_CI_A {name:'Alice', height:20, id:42, _id: 1337, caption: 'hello'})-[:KNOWS {year: 2025, id: 41, source: 1, target: 2}]->"
15+
"(b:_CI_A:_CI_B {name:'Bob', height:10, id: 84, size: 11, labels: [1,2]}), (b)-[:RELATED {year: 2015, _type: 'A', caption:'hej'}]->(a)"
1416
)
1517
yield
1618
neo4j_session.run("MATCH (n:_CI_A|_CI_B) DETACH DELETE n")
@@ -22,11 +24,30 @@ def test_from_neo4j_graph(neo4j_session: Session) -> None:
2224

2325
VG = from_neo4j(graph)
2426

25-
node_ids: list[str] = [node.element_id for node in graph.nodes]
27+
sorted_nodes: list[neo4j.graph.Node] = sorted(graph.nodes, key=lambda x: dict(x.items())["name"])
28+
node_ids: list[str] = [node.element_id for node in sorted_nodes]
2629

2730
expected_nodes = [
28-
Node(id=node_ids[0], caption="_CI_A", labels=["_CI_A"], name="Alice", height=20),
29-
Node(id=node_ids[1], caption="_CI_A:_CI_B", labels=["_CI_A", "_CI_B"], name="Bob", height=10),
31+
Node(
32+
id=node_ids[0],
33+
caption="_CI_A",
34+
labels=["_CI_A"],
35+
name="Alice",
36+
height=20,
37+
__id=42,
38+
_id=1337,
39+
__caption="hello",
40+
),
41+
Node(
42+
id=node_ids[1],
43+
caption="_CI_A:_CI_B",
44+
labels=["_CI_A", "_CI_B"],
45+
name="Bob",
46+
height=10,
47+
__id=84,
48+
__size=11,
49+
__labels=[1, 2],
50+
),
3051
]
3152

3253
assert len(VG.nodes) == 2
@@ -47,11 +68,31 @@ def test_from_neo4j_result(neo4j_session: Session) -> None:
4768
VG = from_neo4j(result)
4869

4970
graph = result.graph()
50-
node_ids: list[str] = [node.element_id for node in graph.nodes]
71+
72+
sorted_nodes: list[neo4j.graph.Node] = sorted(graph.nodes, key=lambda x: dict(x.items())["name"])
73+
node_ids: list[str] = [node.element_id for node in sorted_nodes]
5174

5275
expected_nodes = [
53-
Node(id=node_ids[0], caption="_CI_A", labels=["_CI_A"], name="Alice", height=20),
54-
Node(id=node_ids[1], caption="_CI_A:_CI_B", labels=["_CI_A", "_CI_B"], name="Bob", height=10),
76+
Node(
77+
id=node_ids[0],
78+
caption="_CI_A",
79+
labels=["_CI_A"],
80+
name="Alice",
81+
height=20,
82+
__id=42,
83+
_id=1337,
84+
__caption="hello",
85+
),
86+
Node(
87+
id=node_ids[1],
88+
caption="_CI_A:_CI_B",
89+
labels=["_CI_A", "_CI_B"],
90+
name="Bob",
91+
height=10,
92+
__id=84,
93+
__size=11,
94+
__labels=[1, 2],
95+
),
5596
]
5697

5798
assert len(VG.nodes) == 2
@@ -71,11 +112,32 @@ def test_from_neo4j_graph_full(neo4j_session: Session) -> None:
71112

72113
VG = from_neo4j(graph, node_caption="name", relationship_caption="year", size_property="height")
73114

74-
node_ids: list[str] = [node.element_id for node in graph.nodes]
115+
sorted_nodes: list[neo4j.graph.Node] = sorted(graph.nodes, key=lambda x: dict(x.items())["name"])
116+
node_ids: list[str] = [node.element_id for node in sorted_nodes]
75117

76118
expected_nodes = [
77-
Node(id=node_ids[0], caption="Alice", labels=["_CI_A"], name="Alice", height=20, size=60.0),
78-
Node(id=node_ids[1], caption="Bob", labels=["_CI_A", "_CI_B"], name="Bob", height=10, size=3.0),
119+
Node(
120+
id=node_ids[0],
121+
caption="Alice",
122+
labels=["_CI_A"],
123+
name="Alice",
124+
height=20,
125+
size=60.0,
126+
__id=42,
127+
_id=1337,
128+
__caption="hello",
129+
),
130+
Node(
131+
id=node_ids[1],
132+
caption="Bob",
133+
labels=["_CI_A", "_CI_B"],
134+
name="Bob",
135+
height=10,
136+
size=3.0,
137+
__id=84,
138+
__size=11,
139+
__labels=[1, 2],
140+
),
79141
]
80142

81143
assert len(VG.nodes) == 2

0 commit comments

Comments
 (0)