From 99fbbe4dc99930cfbf5b2743133b86955d479a3d Mon Sep 17 00:00:00 2001 From: Adam Schill Collberg Date: Thu, 22 May 2025 11:16:51 +0200 Subject: [PATCH 1/3] Convert non-json-serializable properties to strings --- changelog.md | 1 + python-wrapper/src/neo4j_viz/nvl.py | 39 +++++++++++++++++-------- python-wrapper/tests/test_render.py | 45 +++++++++++------------------ 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/changelog.md b/changelog.md index eb12c876..cef184c6 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ ## Improvements * Allow for `Node` and `Relationship` inputs to be given as `camelCase` and `SCREAMING_SNAKE_CASE` (in addition to `snake_case`). +* Convert non-json serializable properties to strings in the `render` method, instead of raising an error. ## Other changes diff --git a/python-wrapper/src/neo4j_viz/nvl.py b/python-wrapper/src/neo4j_viz/nvl.py index e7f9b78c..5d03a642 100644 --- a/python-wrapper/src/neo4j_viz/nvl.py +++ b/python-wrapper/src/neo4j_viz/nvl.py @@ -3,6 +3,7 @@ import json import uuid from importlib.resources import files +from typing import Any, Union from IPython.display import HTML @@ -40,10 +41,30 @@ def __init__(self) -> None: with screenshot_path.open("r", encoding="utf-8") as file: self.screenshot_svg = file.read() - def unsupported_field_type_error(self, e: TypeError, entity: str) -> Exception: - if "not JSON serializable" in str(e): - return ValueError(f"A field of a {entity} object is not supported: {str(e)}") - return e + @staticmethod + def _serialize_entity(entity: Union[Node, Relationship]) -> dict[str, Any]: + try: + entity_dict = entity.to_dict() + json.dumps(entity_dict) + return entity_dict + except TypeError: + props_as_strings = {} + for k, v in entity_dict["properties"].items(): + try: + json.dumps(v) + except TypeError: + props_as_strings[k] = str(v) + entity_dict["properties"].update(props_as_strings) + + try: + json.dumps(entity_dict) + return entity_dict + except TypeError as e: + # This should never happen anymore, but just in case + if "not JSON serializable" in str(e): + raise ValueError(f"A field of a {type(entity).__name__} object is not supported: {str(e)}") + else: + raise e def render( self, @@ -54,14 +75,8 @@ def render( height: str, show_hover_tooltip: bool, ) -> HTML: - try: - nodes_json = json.dumps([node.to_dict() for node in nodes]) - except TypeError as e: - raise self.unsupported_field_type_error(e, "node") - try: - rels_json = json.dumps([rel.to_dict() for rel in relationships]) - except TypeError as e: - raise self.unsupported_field_type_error(e, "relationship") + nodes_json = json.dumps([self._serialize_entity(node) for node in nodes]) + rels_json = json.dumps([self._serialize_entity(rel) for rel in relationships]) render_options_json = json.dumps(render_options.to_dict()) container_id = str(uuid.uuid4()) diff --git a/python-wrapper/tests/test_render.py b/python-wrapper/tests/test_render.py index f505dc14..974f6002 100644 --- a/python-wrapper/tests/test_render.py +++ b/python-wrapper/tests/test_render.py @@ -64,34 +64,6 @@ def test_basic_render(render_option: dict[str, Any], tmp_path: Path) -> None: assert not severe_logs, f"Severe logs found: {severe_logs}, all logs: {logs}" -def test_unsupported_field_type() -> None: - with pytest.raises( - ValueError, match="A field of a node object is not supported: Object of type set is not JSON serializable" - ): - nodes = [ - Node( - id="4:d09f48a4-5fca-421d-921d-a30a896c604d:0", caption="Person", properties={"unsupported": {1, 2, 3}} - ), - ] - VG = VisualizationGraph(nodes=nodes, relationships=[]) - VG.render() - - with pytest.raises( - ValueError, - match="A field of a relationship object is not supported: Object of type set is not JSON serializable", - ): - relationships = [ - Relationship( - source="4:d09f48a4-5fca-421d-921d-a30a896c604d:0", - target="4:d09f48a4-5fca-421d-921d-a30a896c604d:6", - caption="BUYS", - properties={"unsupported": {1, 2, 3}}, - ), - ] - VG = VisualizationGraph(nodes=[], relationships=relationships) - VG.render() - - def test_max_allowed_nodes_limit() -> None: nodes = [Node(id=i) for i in range(10_001)] VG = VisualizationGraph(nodes=nodes, relationships=[]) @@ -121,3 +93,20 @@ def test_render_warnings() -> None: "relationships. If you need these features, use the canvas renderer by setting the `renderer` parameter", ): VG.render(max_allowed_nodes=20_000, renderer=Renderer.WEB_GL) + + +def test_render_non_json_serializable() -> None: + import datetime + + nodes = [ + Node( + id=0, + properties={ + "non-json-serializable": datetime.datetime.now(), + }, + ) + ] + + VG = VisualizationGraph(nodes=nodes, relationships=[]) + + VG.render() From 55997d3fc0dca76a7123f7359f1c55c505a0c689 Mon Sep 17 00:00:00 2001 From: Adam Schill Collberg Date: Thu, 22 May 2025 11:44:40 +0200 Subject: [PATCH 2/3] Avoid duplicate `json.dumps` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Florentin Dörre --- python-wrapper/src/neo4j_viz/nvl.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/python-wrapper/src/neo4j_viz/nvl.py b/python-wrapper/src/neo4j_viz/nvl.py index 5d03a642..f8f825d0 100644 --- a/python-wrapper/src/neo4j_viz/nvl.py +++ b/python-wrapper/src/neo4j_viz/nvl.py @@ -3,7 +3,7 @@ import json import uuid from importlib.resources import files -from typing import Any, Union +from typing import Union from IPython.display import HTML @@ -42,11 +42,10 @@ def __init__(self) -> None: self.screenshot_svg = file.read() @staticmethod - def _serialize_entity(entity: Union[Node, Relationship]) -> dict[str, Any]: + def _serialize_entity(entity: Union[Node, Relationship]) -> str: try: entity_dict = entity.to_dict() - json.dumps(entity_dict) - return entity_dict + return json.dumps(entity_dict) except TypeError: props_as_strings = {} for k, v in entity_dict["properties"].items(): @@ -57,8 +56,7 @@ def _serialize_entity(entity: Union[Node, Relationship]) -> dict[str, Any]: entity_dict["properties"].update(props_as_strings) try: - json.dumps(entity_dict) - return entity_dict + return json.dumps(entity_dict) except TypeError as e: # This should never happen anymore, but just in case if "not JSON serializable" in str(e): @@ -75,8 +73,8 @@ def render( height: str, show_hover_tooltip: bool, ) -> HTML: - nodes_json = json.dumps([self._serialize_entity(node) for node in nodes]) - rels_json = json.dumps([self._serialize_entity(rel) for rel in relationships]) + nodes_json = f"[{','.join([self._serialize_entity(node) for node in nodes])}]" + rels_json = f"[{','.join([self._serialize_entity(rel) for rel in relationships])}]" render_options_json = json.dumps(render_options.to_dict()) container_id = str(uuid.uuid4()) From e7474baee9b7f60ad304aa28857ec9fd065021ad Mon Sep 17 00:00:00 2001 From: Adam Schill Collberg Date: Thu, 22 May 2025 11:46:59 +0200 Subject: [PATCH 3/3] Add explicit testing for `NVL._serialize_entity` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Florentin Dörre --- python-wrapper/tests/test_render.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/python-wrapper/tests/test_render.py b/python-wrapper/tests/test_render.py index 974f6002..b04d4b9b 100644 --- a/python-wrapper/tests/test_render.py +++ b/python-wrapper/tests/test_render.py @@ -6,6 +6,7 @@ from selenium import webdriver from neo4j_viz import Node, Relationship, VisualizationGraph +from neo4j_viz.nvl import NVL from neo4j_viz.options import Layout, Renderer render_cases = { @@ -98,15 +99,15 @@ def test_render_warnings() -> None: def test_render_non_json_serializable() -> None: import datetime - nodes = [ - Node( - id=0, - properties={ - "non-json-serializable": datetime.datetime.now(), - }, - ) - ] - - VG = VisualizationGraph(nodes=nodes, relationships=[]) - + now = datetime.datetime.now() + node = Node( + id=0, + properties={ + "non-json-serializable": now, + }, + ) + assert str(now) in NVL._serialize_entity(node) + + VG = VisualizationGraph(nodes=[node], relationships=[]) + # Should not raise an error VG.render()