Skip to content

Commit 0e76e7e

Browse files
Fix DOT sanitized key collisions in importer
Co-authored-by: Dr Matt James <mattkjames7@users.noreply.github.com>
1 parent 49d51ea commit 0e76e7e

2 files changed

Lines changed: 90 additions & 11 deletions

File tree

gqlalchemy/transformations/importing/graph_importer.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,7 @@ def _walk(current_graph) -> None:
157157
if node_id in {"", "node", "edge", "graph"}:
158158
continue
159159

160-
properties = {
161-
self._sanitize_property_key(key): self._normalize_dot_value(value)
162-
for key, value in (node.get_attributes() or {}).items()
163-
}
160+
properties = self._normalize_dot_attributes(node.get_attributes() or {})
164161
if node_id in graph.nodes:
165162
graph.nodes[node_id].update(properties)
166163
else:
@@ -172,10 +169,7 @@ def _walk(current_graph) -> None:
172169
if not source or not dest:
173170
continue
174171

175-
properties = {
176-
self._sanitize_property_key(key): self._normalize_dot_value(value)
177-
for key, value in (edge.get_attributes() or {}).items()
178-
}
172+
properties = self._normalize_dot_attributes(edge.get_attributes() or {})
179173
graph.add_edge(source, dest, **properties)
180174

181175
for subgraph in current_graph.get_subgraphs():
@@ -186,9 +180,7 @@ def _walk(current_graph) -> None:
186180
return graph
187181

188182
def _normalize_dot_properties(self, properties: Dict[str, Any]) -> Dict[str, Any]:
189-
normalized_attributes = {
190-
self._sanitize_property_key(key): self._normalize_dot_value(value) for key, value in properties.items()
191-
}
183+
normalized_attributes = self._normalize_dot_attributes(properties)
192184
normalized_properties: Dict[str, Any] = dict(normalized_attributes)
193185
normalized_properties["attributes_json"] = json.dumps(normalized_attributes, sort_keys=True)
194186

@@ -200,6 +192,23 @@ def _normalize_dot_properties(self, properties: Dict[str, Any]) -> Dict[str, Any
200192

201193
return normalized_properties
202194

195+
def _normalize_dot_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
196+
normalized_attributes: Dict[str, Any] = {}
197+
for key, value in attributes.items():
198+
sanitized_key = self._sanitize_property_key(key)
199+
unique_key = self._resolve_key_collision(sanitized_key, normalized_attributes)
200+
normalized_attributes[unique_key] = self._normalize_dot_value(value)
201+
return normalized_attributes
202+
203+
@staticmethod
204+
def _resolve_key_collision(base_key: str, properties: Dict[str, Any]) -> str:
205+
unique_key = base_key
206+
suffix = 1
207+
while unique_key in properties:
208+
unique_key = f"{base_key}_{suffix}"
209+
suffix += 1
210+
return unique_key
211+
203212
@staticmethod
204213
def _normalize_dot_value(value: Any) -> Any:
205214
if isinstance(value, str):

tests/transformations/importing/test_import.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,76 @@ def test_import_nx_normalize_dot_properties_json_key_collision():
5757
assert normalized_properties["attributes_label"] == "A"
5858

5959

60+
def test_import_nx_normalize_dot_properties_sanitized_key_collision_preserves_all_values():
61+
importer = GraphImporter(graph_type="Nx")
62+
63+
normalized_properties = importer._normalize_dot_properties({"my.attr": "dot-value", "my-attr": "dash-value"})
64+
65+
assert json.loads(normalized_properties["attributes_json"]) == {"my_attr": "dot-value", "my_attr_1": "dash-value"}
66+
assert normalized_properties["attributes_my_attr"] == "dot-value"
67+
assert normalized_properties["attributes_my_attr_1"] == "dash-value"
68+
69+
70+
def test_import_nx_graph_from_pydot_sanitized_key_collision_preserves_all_values():
71+
importer = GraphImporter(graph_type="Nx")
72+
73+
class _DummyNode:
74+
def __init__(self, name, attributes):
75+
self._name = name
76+
self._attributes = attributes
77+
78+
def get_name(self):
79+
return self._name
80+
81+
def get_attributes(self):
82+
return self._attributes
83+
84+
class _DummyEdge:
85+
def __init__(self, source, destination, attributes):
86+
self._source = source
87+
self._destination = destination
88+
self._attributes = attributes
89+
90+
def get_source(self):
91+
return self._source
92+
93+
def get_destination(self):
94+
return self._destination
95+
96+
def get_attributes(self):
97+
return self._attributes
98+
99+
class _DummyDotGraph:
100+
def __init__(self, nodes, edges):
101+
self._nodes = nodes
102+
self._edges = edges
103+
104+
def get_nodes(self):
105+
return self._nodes
106+
107+
def get_edges(self):
108+
return self._edges
109+
110+
def get_subgraphs(self):
111+
return []
112+
113+
dot_graph = _DummyDotGraph(
114+
nodes=[
115+
_DummyNode("A", {"my.attr": "dot-value", "my-attr": "dash-value"}),
116+
_DummyNode("B", {}),
117+
],
118+
edges=[_DummyEdge("A", "B", {"edge.attr": "left", "edge-attr": "right"})],
119+
)
120+
121+
graph = importer._graph_from_pydot(dot_graph)
122+
edge_data = next(iter(graph.edges(data=True)))[2]
123+
124+
assert graph.nodes["A"]["my_attr"] == "dot-value"
125+
assert graph.nodes["A"]["my_attr_1"] == "dash-value"
126+
assert edge_data["edge_attr"] == "left"
127+
assert edge_data["edge_attr_1"] == "right"
128+
129+
60130
def test_import_nx_normalize_dot_graph_for_digraph():
61131
importer = GraphImporter(graph_type="Nx")
62132
dot_graph = nx.DiGraph()

0 commit comments

Comments
 (0)