From c2abff836ebeae38b88d840f598571daaaa51ea3 Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Fri, 24 Jul 2026 10:52:39 +0200 Subject: [PATCH 01/15] feat: add .view() --- src/flowrep/retrospective/datastructures.py | 52 ++++++++++++++++++++- tests/unit/test_retrospective_wfms.py | 45 ++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/flowrep/retrospective/datastructures.py b/src/flowrep/retrospective/datastructures.py index cc01d61e..e36ec962 100644 --- a/src/flowrep/retrospective/datastructures.py +++ b/src/flowrep/retrospective/datastructures.py @@ -17,7 +17,7 @@ import dataclasses import inspect import types -from collections.abc import Callable, MutableMapping +from collections.abc import Callable, Mapping, MutableMapping, Sequence from typing import Any, Generic, Self, TypeVar, get_args, get_origin, get_type_hints from pyiron_snippets import retrieve, singleton @@ -92,6 +92,13 @@ class NodeData(Generic[RecipeType], abc.ABC): @abc.abstractmethod def from_recipe(cls, recipe: RecipeType) -> Self: ... + def view(self, expanded: bool = False): + """ + Display this data object as structured JSON in a notebook and return the display + object. + """ + return _display_json(_to_jsonable(self), expanded=expanded) + def recipe2data( recipe: union_types.RecipeDiscrimination, allow_variadic_inputs: bool = True @@ -385,3 +392,46 @@ def _parse_return_dataclass( label: OutputDataPort(annotation=hints.get(field.name, None)) for label, field in zip(outputs, fields, strict=True) } + + +def _to_jsonable(data: Any) -> Any: + if data is None or isinstance(data, bool | int | float | str): + return data + if isinstance(data, Mapping): + return { + _to_jsonable_key(key): _to_jsonable(value) for key, value in data.items() + } + if isinstance(data, Sequence) and not isinstance(data, str): + return [_to_jsonable(item) for item in data] + if dataclasses.is_dataclass(data) and not isinstance(data, type): + serialized = { + field.name: _to_jsonable(getattr(data, field.name)) + for field in dataclasses.fields(data) + } + serialized["type"] = data.__class__.__name__ + return serialized + if hasattr(data, "model_dump"): + model_dump = data.model_dump(mode="json") + if isinstance(model_dump, Mapping): + model_dump["type"] = data.__class__.__name__ + return _to_jsonable(model_dump) + if isinstance(data, type): + return f"{data.__module__}.{data.__qualname__}" + if inspect.isroutine(data): + return f"{data.__module__}.{data.__qualname__}" + return repr(data) + + +def _to_jsonable_key(key: Any) -> str: + jsonable = _to_jsonable(key) + if isinstance(jsonable, str): + return jsonable + return repr(jsonable) + + +def _display_json(data: Any, *, expanded: bool = False): + try: + from IPython.display import JSON + except ImportError as e: + raise ImportError("This tool requires the 'IPython' package.") from e + return JSON(data, expanded=expanded) diff --git a/tests/unit/test_retrospective_wfms.py b/tests/unit/test_retrospective_wfms.py index 46a9d0f5..33091ad8 100644 --- a/tests/unit/test_retrospective_wfms.py +++ b/tests/unit/test_retrospective_wfms.py @@ -5,6 +5,7 @@ import dataclasses import pickle import unittest +from unittest import mock from typing import TYPE_CHECKING, get_origin from pyiron_snippets import versions @@ -28,6 +29,13 @@ if TYPE_CHECKING: from pyiron_snippets.colors import SeabornColors +try: + from IPython.display import JSON as IPythonJSON + + _has_ipython = True +except ImportError: + _has_ipython = False + # ═══════════════════════════════════════════════════════════════════════════ # Unavailable annotations @@ -849,6 +857,43 @@ def test_propagates_into_workflow_children(self): self.assertIsInstance(wf.nodes["splitter_0"], datastructures.AtomicData) +@unittest.skipUnless(_has_ipython, "IPython not installed") +class TestDataView(unittest.TestCase): + def test_node_data_view(self): + node = datastructures.AtomicData.from_recipe(library.identity.flowrep_recipe) + with mock.patch( + "flowrep.retrospective.datastructures._display_json" + ) as mocked_display: + mocked_display.return_value = "shown" + shown = node.view(expanded=True) + self.assertEqual(shown, "shown") + mocked_display.assert_called_once_with(mock.ANY, expanded=True) + data = mocked_display.call_args.args[0] + self.assertEqual(data["type"], "AtomicData") + self.assertIn("input_ports", data) + + def test_composite_data_view(self): + node = datastructures.DagData.from_recipe(_linear_workflow()) + with mock.patch( + "flowrep.retrospective.datastructures._display_json" + ) as mocked_display: + mocked_display.return_value = "shown" + shown = node.view() + self.assertEqual(shown, "shown") + mocked_display.assert_called_once() + data = mocked_display.call_args.args[0] + self.assertEqual(data["type"], "DagData") + self.assertIn("nodes", data) + self.assertIn("add_0", data["nodes"]) + + def test_display_json_helper(self): + shown = datastructures._display_json({"x": 1}, expanded=True) + self.assertIsInstance(shown, IPythonJSON) + self.assertEqual(shown.data, {"x": 1}) + _, metadata = shown._repr_json_() + self.assertTrue(metadata["expanded"]) + + # ═══════════════════════════════════════════════════════════════════════════ # wfms.py tests — atomic # ═══════════════════════════════════════════════════════════════════════════ From 0cb41151e2c1475d7a30e56fbee68d9d97e60034 Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Fri, 24 Jul 2026 11:15:32 +0200 Subject: [PATCH 02/15] test: restore identity --- tests/flowrep_static/library.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/flowrep_static/library.py b/tests/flowrep_static/library.py index 3e6be75d..e617125c 100644 --- a/tests/flowrep_static/library.py +++ b/tests/flowrep_static/library.py @@ -17,6 +17,11 @@ def multi_result(x): return a, b +@atomic_parser.atomic +def identity(x): + return x + + @atomic_parser.atomic def my_range(n): return list(range(n)) From 882e8031c10d778c1246b9cf01346d26a50d9c55 Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Fri, 24 Jul 2026 11:27:32 +0200 Subject: [PATCH 03/15] style: ruff --- tests/unit/test_retrospective_wfms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_retrospective_wfms.py b/tests/unit/test_retrospective_wfms.py index 1ddd8374..e6cd1a2c 100644 --- a/tests/unit/test_retrospective_wfms.py +++ b/tests/unit/test_retrospective_wfms.py @@ -5,8 +5,8 @@ import dataclasses import pickle import unittest -from unittest import mock from typing import TYPE_CHECKING, NamedTuple, get_origin +from unittest import mock from pyiron_snippets import versions From b09885474c8915d34479ee47e1bcb51ce07309f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:33:34 +0000 Subject: [PATCH 04/15] fix: satisfy mypy in data view serialization --- src/flowrep/retrospective/datastructures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flowrep/retrospective/datastructures.py b/src/flowrep/retrospective/datastructures.py index 4870f5f3..552ba5cb 100644 --- a/src/flowrep/retrospective/datastructures.py +++ b/src/flowrep/retrospective/datastructures.py @@ -417,7 +417,7 @@ def _to_jsonable(data: Any) -> Any: if hasattr(data, "model_dump"): model_dump = data.model_dump(mode="json") if isinstance(model_dump, Mapping): - model_dump["type"] = data.__class__.__name__ + model_dump = {**model_dump, "type": data.__class__.__name__} return _to_jsonable(model_dump) if isinstance(data, type): return f"{data.__module__}.{data.__qualname__}" From dd6b6ed9659f6b56fd7626380b1297743b7a498e Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Sat, 25 Jul 2026 08:55:59 +0200 Subject: [PATCH 05/15] fix: remove identity again following Liam's comment --- tests/flowrep_static/library.py | 5 ----- tests/unit/test_retrospective_wfms.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/flowrep_static/library.py b/tests/flowrep_static/library.py index e617125c..3e6be75d 100644 --- a/tests/flowrep_static/library.py +++ b/tests/flowrep_static/library.py @@ -17,11 +17,6 @@ def multi_result(x): return a, b -@atomic_parser.atomic -def identity(x): - return x - - @atomic_parser.atomic def my_range(n): return list(range(n)) diff --git a/tests/unit/test_retrospective_wfms.py b/tests/unit/test_retrospective_wfms.py index e6cd1a2c..d6815b45 100644 --- a/tests/unit/test_retrospective_wfms.py +++ b/tests/unit/test_retrospective_wfms.py @@ -852,7 +852,7 @@ def test_propagates_into_workflow_children(self): @unittest.skipUnless(_has_ipython, "IPython not installed") class TestDataView(unittest.TestCase): def test_node_data_view(self): - node = datastructures.AtomicData.from_recipe(library.identity.flowrep_recipe) + node = datastructures.AtomicData.from_recipe(std.identity.flowrep_recipe) with mock.patch( "flowrep.retrospective.datastructures._display_json" ) as mocked_display: From 1d0e854ff6bbc6b8741904ca7de17d3b64742b07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 07:20:27 +0000 Subject: [PATCH 06/15] feat: refactor view() into viewer module, add str fallback, formalize dataviewer optional dep --- .ci_support/environment-optional.yml | 1 + pyproject.toml | 3 + src/flowrep/retrospective/datastructures.py | 50 ++----------- src/flowrep/retrospective/viewer.py | 77 +++++++++++++++++++++ tests/unit/test_retrospective_wfms.py | 49 ++++++++----- 5 files changed, 119 insertions(+), 61 deletions(-) create mode 100644 src/flowrep/retrospective/viewer.py diff --git a/.ci_support/environment-optional.yml b/.ci_support/environment-optional.yml index 155a07b4..4261aa03 100644 --- a/.ci_support/environment-optional.yml +++ b/.ci_support/environment-optional.yml @@ -2,5 +2,6 @@ channels: - conda-forge dependencies: - bagofholding =0.1.12 +- ipython - ipytree =0.2.2 - python-workflow-definition =0.1.5 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 165e6184..e0cff9d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,9 @@ storage-widget = [ "flowrep[storage]", "ipytree==0.2.2", ] +dataviewer = [ + "ipython", +] [tool.hatch.build] include = [ diff --git a/src/flowrep/retrospective/datastructures.py b/src/flowrep/retrospective/datastructures.py index 552ba5cb..633711d7 100644 --- a/src/flowrep/retrospective/datastructures.py +++ b/src/flowrep/retrospective/datastructures.py @@ -17,7 +17,7 @@ import dataclasses import inspect import types -from collections.abc import Callable, Mapping, MutableMapping, Sequence +from collections.abc import Callable, Mapping, MutableMapping from typing import Any, Generic, Self, TypeVar, get_args, get_origin, get_type_hints from pyiron_snippets import retrieve, singleton @@ -96,9 +96,11 @@ def from_recipe(cls, recipe: RecipeType) -> Self: ... def view(self, expanded: bool = False): """ Display this data object as structured JSON in a notebook and return the display - object. + object. Falls back to a plain-text representation when IPython is not available. """ - return _display_json(_to_jsonable(self), expanded=expanded) + from flowrep.retrospective import viewer + + return viewer.view(self, expanded=expanded) def recipe2data( @@ -398,44 +400,4 @@ def _parse_return_tuple( return output_ports -def _to_jsonable(data: Any) -> Any: - if data is None or isinstance(data, bool | int | float | str): - return data - if isinstance(data, Mapping): - return { - _to_jsonable_key(key): _to_jsonable(value) for key, value in data.items() - } - if isinstance(data, Sequence) and not isinstance(data, str): - return [_to_jsonable(item) for item in data] - if dataclasses.is_dataclass(data) and not isinstance(data, type): - serialized = { - field.name: _to_jsonable(getattr(data, field.name)) - for field in dataclasses.fields(data) - } - serialized["type"] = data.__class__.__name__ - return serialized - if hasattr(data, "model_dump"): - model_dump = data.model_dump(mode="json") - if isinstance(model_dump, Mapping): - model_dump = {**model_dump, "type": data.__class__.__name__} - return _to_jsonable(model_dump) - if isinstance(data, type): - return f"{data.__module__}.{data.__qualname__}" - if inspect.isroutine(data): - return f"{data.__module__}.{data.__qualname__}" - return repr(data) - - -def _to_jsonable_key(key: Any) -> str: - jsonable = _to_jsonable(key) - if isinstance(jsonable, str): - return jsonable - return repr(jsonable) - - -def _display_json(data: Any, *, expanded: bool = False): - try: - from IPython.display import JSON - except ImportError as e: - raise ImportError("This tool requires the 'IPython' package.") from e - return JSON(data, expanded=expanded) + diff --git a/src/flowrep/retrospective/viewer.py b/src/flowrep/retrospective/viewer.py new file mode 100644 index 00000000..347fcb1e --- /dev/null +++ b/src/flowrep/retrospective/viewer.py @@ -0,0 +1,77 @@ +""" +Viewer helpers for retrospective data objects. + +Provides a notebook-friendly JSON view and a plain-text fallback. +""" + +from __future__ import annotations + +import dataclasses +import inspect +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from flowrep.retrospective.datastructures import NodeData + +try: + from IPython.display import JSON as _JSON + + _has_ipython = True +except ImportError: + _has_ipython = False + + +def _to_jsonable(data: Any) -> Any: + if data is None or isinstance(data, bool | int | float | str): + return data + if isinstance(data, Mapping): + return { + _to_jsonable_key(key): _to_jsonable(value) for key, value in data.items() + } + if isinstance(data, Sequence) and not isinstance(data, str): + return [_to_jsonable(item) for item in data] + if dataclasses.is_dataclass(data) and not isinstance(data, type): + serialized = { + field.name: _to_jsonable(getattr(data, field.name)) + for field in dataclasses.fields(data) + } + serialized["type"] = data.__class__.__name__ + return serialized + if hasattr(data, "model_dump"): + model_dump = data.model_dump(mode="json") + if isinstance(model_dump, Mapping): + model_dump = {**model_dump, "type": data.__class__.__name__} + return _to_jsonable(model_dump) + if isinstance(data, type): + return f"{data.__module__}.{data.__qualname__}" + if inspect.isroutine(data): + return f"{data.__module__}.{data.__qualname__}" + return repr(data) + + +def _to_jsonable_key(key: Any) -> str: + jsonable = _to_jsonable(key) + if isinstance(jsonable, str): + return jsonable + return repr(jsonable) + + +def _view_json(data: NodeData, *, expanded: bool = False): + """Display *data* as structured JSON (requires IPython).""" + return _JSON(_to_jsonable(data), expanded=expanded) + + +def _view_str(data: NodeData) -> str: + """Return a plain-text representation of *data*.""" + return str(data) + + +def view(data: NodeData, *, expanded: bool = False): + """ + Display *data* as structured JSON when IPython is available, otherwise + return a plain-text representation. + """ + if _has_ipython: + return _view_json(data, expanded=expanded) + return _view_str(data) diff --git a/tests/unit/test_retrospective_wfms.py b/tests/unit/test_retrospective_wfms.py index d6815b45..a16fe756 100644 --- a/tests/unit/test_retrospective_wfms.py +++ b/tests/unit/test_retrospective_wfms.py @@ -853,39 +853,54 @@ def test_propagates_into_workflow_children(self): class TestDataView(unittest.TestCase): def test_node_data_view(self): node = datastructures.AtomicData.from_recipe(std.identity.flowrep_recipe) - with mock.patch( - "flowrep.retrospective.datastructures._display_json" - ) as mocked_display: - mocked_display.return_value = "shown" - shown = node.view(expanded=True) - self.assertEqual(shown, "shown") - mocked_display.assert_called_once_with(mock.ANY, expanded=True) - data = mocked_display.call_args.args[0] + shown = node.view(expanded=True) + self.assertIsInstance(shown, IPythonJSON) + data = shown.data self.assertEqual(data["type"], "AtomicData") self.assertIn("input_ports", data) def test_composite_data_view(self): node = datastructures.DagData.from_recipe(_linear_workflow()) - with mock.patch( - "flowrep.retrospective.datastructures._display_json" - ) as mocked_display: - mocked_display.return_value = "shown" - shown = node.view() - self.assertEqual(shown, "shown") - mocked_display.assert_called_once() - data = mocked_display.call_args.args[0] + shown = node.view() + self.assertIsInstance(shown, IPythonJSON) + data = shown.data self.assertEqual(data["type"], "DagData") self.assertIn("nodes", data) self.assertIn("add_0", data["nodes"]) + def test_view_expanded_flag(self): + node = datastructures.AtomicData.from_recipe(std.identity.flowrep_recipe) + shown = node.view(expanded=True) + _, metadata = shown._repr_json_() + self.assertTrue(metadata["expanded"]) + def test_display_json_helper(self): - shown = datastructures._display_json({"x": 1}, expanded=True) + from flowrep.retrospective import viewer + + shown = viewer._view_json({"x": 1}, expanded=True) self.assertIsInstance(shown, IPythonJSON) self.assertEqual(shown.data, {"x": 1}) _, metadata = shown._repr_json_() self.assertTrue(metadata["expanded"]) +class TestViewerStrFallback(unittest.TestCase): + def test_view_str_returns_string(self): + from flowrep.retrospective import viewer + + node = datastructures.AtomicData.from_recipe(std.identity.flowrep_recipe) + result = viewer._view_str(node) + self.assertIsInstance(result, str) + + def test_view_without_ipython_falls_back_to_str(self): + from flowrep.retrospective import viewer + + node = datastructures.AtomicData.from_recipe(std.identity.flowrep_recipe) + with mock.patch.object(viewer, "_has_ipython", False): + result = viewer.view(node) + self.assertIsInstance(result, str) + + # ═══════════════════════════════════════════════════════════════════════════ # wfms.py tests — atomic # ═══════════════════════════════════════════════════════════════════════════ From 5dd9aef3b307c993ada76dff5302ac68179a3fa1 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Sun, 26 Jul 2026 04:45:25 +0000 Subject: [PATCH 07/15] [dependabot skip] Update env file --- .binder/environment.yml | 1 + docs/environment.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.binder/environment.yml b/.binder/environment.yml index 2c868c6c..fdc368c4 100644 --- a/.binder/environment.yml +++ b/.binder/environment.yml @@ -9,6 +9,7 @@ dependencies: - pyiron_snippets =1.4.0 - typing-extensions =4.16.0 - bagofholding =0.1.12 +- ipython - ipytree =0.2.2 - python-workflow-definition =0.1.5 - numpy =2.4.6 diff --git a/docs/environment.yml b/docs/environment.yml index 37ac272f..fa833e4b 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -15,5 +15,6 @@ dependencies: - pyiron_snippets =1.4.0 - typing-extensions =4.16.0 - bagofholding =0.1.12 +- ipython - ipytree =0.2.2 - python-workflow-definition =0.1.5 From a633eec6565e2c23c6d7047cb0f7e9327d30975b Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Sun, 26 Jul 2026 06:49:31 +0200 Subject: [PATCH 08/15] style: move viewer to the top (and run black) --- src/flowrep/retrospective/datastructures.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/flowrep/retrospective/datastructures.py b/src/flowrep/retrospective/datastructures.py index 633711d7..aad85812 100644 --- a/src/flowrep/retrospective/datastructures.py +++ b/src/flowrep/retrospective/datastructures.py @@ -33,6 +33,7 @@ while_recipe, workflow_recipe, ) +from flowrep.retrospective import viewer class NotData(metaclass=singleton.Singleton): @@ -98,8 +99,6 @@ def view(self, expanded: bool = False): Display this data object as structured JSON in a notebook and return the display object. Falls back to a plain-text representation when IPython is not available. """ - from flowrep.retrospective import viewer - return viewer.view(self, expanded=expanded) @@ -398,6 +397,3 @@ def _parse_return_tuple( else: output_ports = {outputs[0]: OutputDataPort(annotation=return_annotation)} return output_ports - - - From e0deed3eb579512dd97d7ce861ce54ea78fd27ec Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Sun, 26 Jul 2026 06:55:44 +0200 Subject: [PATCH 09/15] feat: implement _repr_json_ --- src/flowrep/retrospective/datastructures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/flowrep/retrospective/datastructures.py b/src/flowrep/retrospective/datastructures.py index aad85812..71af1393 100644 --- a/src/flowrep/retrospective/datastructures.py +++ b/src/flowrep/retrospective/datastructures.py @@ -101,6 +101,9 @@ def view(self, expanded: bool = False): """ return viewer.view(self, expanded=expanded) + def _repr_json_(self): + return self.view()._repr_json_() + def recipe2data( recipe: union_types.RecipeDiscrimination, allow_variadic_inputs: bool = True From 0f797b9d474acb422fb0f2b3037dc0e2421f1c42 Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Sun, 26 Jul 2026 07:02:38 +0200 Subject: [PATCH 10/15] style: remove collections.abc.Mapping (because it's not used) --- src/flowrep/retrospective/datastructures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flowrep/retrospective/datastructures.py b/src/flowrep/retrospective/datastructures.py index 71af1393..0214976d 100644 --- a/src/flowrep/retrospective/datastructures.py +++ b/src/flowrep/retrospective/datastructures.py @@ -17,7 +17,7 @@ import dataclasses import inspect import types -from collections.abc import Callable, Mapping, MutableMapping +from collections.abc import Callable, MutableMapping from typing import Any, Generic, Self, TypeVar, get_args, get_origin, get_type_hints from pyiron_snippets import retrieve, singleton From 39dd9671b9f5808d9329c29f1ca0532dd859a563 Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Sun, 26 Jul 2026 12:42:50 -0700 Subject: [PATCH 11/15] No cov on without ipython branch Signed-off-by: Liam Huber --- src/flowrep/retrospective/viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flowrep/retrospective/viewer.py b/src/flowrep/retrospective/viewer.py index 347fcb1e..3abc987e 100644 --- a/src/flowrep/retrospective/viewer.py +++ b/src/flowrep/retrospective/viewer.py @@ -18,7 +18,7 @@ from IPython.display import JSON as _JSON _has_ipython = True -except ImportError: +except ImportError: # pragma: no cover _has_ipython = False From 96bfc265eb1deb38efdab330ad7ebefa3ccdac9c Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Sun, 26 Jul 2026 12:43:07 -0700 Subject: [PATCH 12/15] Move import to top Signed-off-by: Liam Huber --- tests/unit/test_retrospective_wfms.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_retrospective_wfms.py b/tests/unit/test_retrospective_wfms.py index a16fe756..14214506 100644 --- a/tests/unit/test_retrospective_wfms.py +++ b/tests/unit/test_retrospective_wfms.py @@ -22,7 +22,7 @@ while_recipe, workflow_recipe, ) -from flowrep.retrospective import datastructures +from flowrep.retrospective import datastructures, viewer from flowrep.retrospective.datastructures import NOT_DATA from flowrep_static import library @@ -875,8 +875,6 @@ def test_view_expanded_flag(self): self.assertTrue(metadata["expanded"]) def test_display_json_helper(self): - from flowrep.retrospective import viewer - shown = viewer._view_json({"x": 1}, expanded=True) self.assertIsInstance(shown, IPythonJSON) self.assertEqual(shown.data, {"x": 1}) From db4147b27ec5e0f98b8de06ebfc9285c78618023 Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Sun, 26 Jul 2026 12:44:54 -0700 Subject: [PATCH 13/15] Test type branch Signed-off-by: Liam Huber --- tests/unit/test_retrospective_wfms.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/test_retrospective_wfms.py b/tests/unit/test_retrospective_wfms.py index 14214506..b6f488d9 100644 --- a/tests/unit/test_retrospective_wfms.py +++ b/tests/unit/test_retrospective_wfms.py @@ -881,6 +881,12 @@ def test_display_json_helper(self): _, metadata = shown._repr_json_() self.assertTrue(metadata["expanded"]) + def test_type_branch(self): + class MyType: ... + + jsond = viewer._to_jsonable(MyType) + self.assertEqual(jsond, f"{MyType.__module__}.{MyType.__qualname__}") + class TestViewerStrFallback(unittest.TestCase): def test_view_str_returns_string(self): From cd3eab6152652289aed190ebab142f3e5526baee Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Sun, 26 Jul 2026 12:49:02 -0700 Subject: [PATCH 14/15] Test non-string key branch Signed-off-by: Liam Huber --- tests/unit/test_retrospective_wfms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_retrospective_wfms.py b/tests/unit/test_retrospective_wfms.py index b6f488d9..895a6cd2 100644 --- a/tests/unit/test_retrospective_wfms.py +++ b/tests/unit/test_retrospective_wfms.py @@ -887,6 +887,9 @@ class MyType: ... jsond = viewer._to_jsonable(MyType) self.assertEqual(jsond, f"{MyType.__module__}.{MyType.__qualname__}") + def test_non_string_key_falls_back_to_repr(self): + self.assertEqual(viewer._to_jsonable({42: "v"}), {"42": "v"}) + class TestViewerStrFallback(unittest.TestCase): def test_view_str_returns_string(self): From 784c3f4b4cf50236d81a7093f6cafd5562daa852 Mon Sep 17 00:00:00 2001 From: Liam Huber Date: Sun, 26 Jul 2026 13:06:33 -0700 Subject: [PATCH 15/15] test repr_json Signed-off-by: Liam Huber --- tests/unit/test_retrospective_wfms.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/test_retrospective_wfms.py b/tests/unit/test_retrospective_wfms.py index 895a6cd2..f233c7b9 100644 --- a/tests/unit/test_retrospective_wfms.py +++ b/tests/unit/test_retrospective_wfms.py @@ -874,6 +874,12 @@ def test_view_expanded_flag(self): _, metadata = shown._repr_json_() self.assertTrue(metadata["expanded"]) + def test_repr_json(self): + node = datastructures.AtomicData.from_recipe(std.identity.flowrep_recipe) + repr_json = node._repr_json_() + self.assertIsInstance(repr_json, tuple) + self.assertEqual(len(repr_json), 2) + def test_display_json_helper(self): shown = viewer._view_json({"x": 1}, expanded=True) self.assertIsInstance(shown, IPythonJSON)