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/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/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" 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,