From 36684a6b6800685ada27ba303d8c1209acab2e32 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Thu, 3 Sep 2026 10:11:09 -0700 Subject: [PATCH 1/2] Render figure PNGs before starting result uploads in log_async The test-suite runner awaits TestResult.log_async() directly, bypassing the pre-serialization that TestResult.log() does. Figures were therefore rendered inside alog_figure while the log_test_results request was already in flight. Kaleido 1.x launches a Chromium per figure and blocks the event loop while it runs, so with several figures the 30s request timeout expired on the results upload (asyncio.TimeoutError from aiohttp's TimerContext). Move the pre-serialization into a helper called from both log() and log_async(), so every PNG is rendered before any request starts. Co-Authored-By: Claude Fable 5.1 --- tests/test_results.py | 29 ++++++++++++++++++++++++++++ validmind/vm_models/result/result.py | 19 ++++++++++++++---- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/tests/test_results.py b/tests/test_results.py index c0632bdd7..970197d6e 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -202,6 +202,35 @@ async def test_test_result_log_async( mock_test_result.assert_called_once() mock_metric.assert_called_once() + @patch("validmind.vm_models.result.result.update_metadata") + @patch("validmind.api_client.alog_test_result") + @patch("validmind.api_client.alog_figure") + async def test_test_result_log_async_pre_serializes_figures( + self, mock_figure, mock_test_result, mock_update_metadata + ): + """Figure PNGs are rendered before any upload request starts""" + mock_test_result.return_value = MockAsyncResponse(200, json={"cuid": "123"}) + mock_update_metadata.return_value = None + cached_at_upload = [] + + async def record(figure): + cached_at_upload.append(figure._cached_png_bytes is not None) + return MockAsyncResponse(200, json={"cuid": "456"}) + + mock_figure.side_effect = record + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + test_result = TestResult( + result_id="test_1", + figures=[Figure(key="fig_1", figure=fig, ref_id="ref_1")], + ) + + await test_result.log_async(section_id="section_1", position=0) + + self.assertEqual(cached_at_upload, [True]) + plt.close(fig) + def test_text_generation_result(self): """Test TextGenerationResult initialization and methods""" text_result = TextGenerationResult( diff --git a/validmind/vm_models/result/result.py b/validmind/vm_models/result/result.py index e60533ba6..c7e38eca1 100644 --- a/validmind/vm_models/result/result.py +++ b/validmind/vm_models/result/result.py @@ -542,6 +542,18 @@ def serialize(self): return serialized + def _pre_serialize_figures(self): + """Render figure PNGs before any upload request starts. + + Kaleido launches a Chromium per figure and blocks the event loop while it + runs. Rendering while uploads are in flight lets the request timeout + expire on them, and with older kaleido it conflicted with asyncio + outright (ZD-626). log() calls this outside the loop; log_async() calls + it too because the test-suite runner awaits log_async() directly. + """ + for figure in self.figures or []: + figure.pre_serialize() + async def log_async( self, section_id: str = None, @@ -553,6 +565,8 @@ async def log_async( if self._is_scorer_result: return + self._pre_serialize_figures() + tasks = [] # collect tasks to run in parallel (async) # Default empty dict if None @@ -680,10 +694,7 @@ def log( # noqa: C901 if section_id: self._validate_section_id_for_block(section_id, position) - # Pre-serialize figures before entering async context to avoid conflicts - # between Plotly's kaleido library and asyncio event loops (ZD-626) - for figure in self.figures or []: - figure.pre_serialize() + self._pre_serialize_figures() run_async( self.log_async, From feb2f06a21bc29e1868aed1ef52520d66dccbfdd Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Thu, 3 Sep 2026 10:20:42 -0700 Subject: [PATCH 2/2] 2.13.13 --- pyproject.toml | 2 +- r/validmind/DESCRIPTION | 2 +- uv.lock | 2 +- validmind/__version__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1ec0fa068..ce818e0dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "validmind" -version = "2.13.12" +version = "2.13.13" description = "ValidMind Library" readme = "README.pypi.md" requires-python = ">=3.9,<3.15" diff --git a/r/validmind/DESCRIPTION b/r/validmind/DESCRIPTION index ecfae362e..618acbcef 100644 --- a/r/validmind/DESCRIPTION +++ b/r/validmind/DESCRIPTION @@ -1,7 +1,7 @@ Package: validmind Type: Package Title: Interface to the 'ValidMind' Platform -Version: 2.13.12 +Version: 2.13.13 Authors@R: c(person("Andres", "Rodriguez", role = c("aut", "cre","cph"), email = "andres@validmind.ai")) Maintainer: Andres Rodriguez diff --git a/uv.lock b/uv.lock index 24c4e25c3..28242e3b4 100644 --- a/uv.lock +++ b/uv.lock @@ -11268,7 +11268,7 @@ wheels = [ [[package]] name = "validmind" -version = "2.13.12" +version = "2.13.13" source = { editable = "." } dependencies = [ { name = "aiohttp", extra = ["speedups"] }, diff --git a/validmind/__version__.py b/validmind/__version__.py index 896ad7fd7..dd7a8b6cd 100644 --- a/validmind/__version__.py +++ b/validmind/__version__.py @@ -1 +1 @@ -__version__ = "2.13.12" +__version__ = "2.13.13"