diff --git a/changelog.md b/changelog.md index eb12c87..cef184c 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 e7f9b78..f8f825d 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 Union from IPython.display import HTML @@ -40,10 +41,28 @@ 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]) -> str: + try: + entity_dict = entity.to_dict() + return json.dumps(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: + 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): + raise ValueError(f"A field of a {type(entity).__name__} object is not supported: {str(e)}") + else: + raise e def render( self, @@ -54,14 +73,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 = 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()) diff --git a/python-wrapper/tests/test_render.py b/python-wrapper/tests/test_render.py index f505dc1..b04d4b9 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 = { @@ -64,34 +65,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 +94,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 + + 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()