diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index 5bedf62b..d366536f 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -155,6 +155,7 @@ def __init__( self._annotation_adapter: BaseMultimodalAnnotationAdapter | None = None self._overwrite = overwrite self._annotation: dict | None = None + self._set_component_called = False def _set_small_annotation_adapter(self, annotation: dict | None = None): self._annotation_adapter = MultimodalSmallAnnotationAdapter( @@ -224,7 +225,9 @@ def save(self): self._set_large_annotation_adapter(self.annotation) else: self._set_small_annotation_adapter(self.annotation) - self._annotation_adapter.save() + if self._set_component_called: + self._annotation_adapter.save() + self._set_component_called = False def get_metadata(self): """ @@ -284,6 +287,7 @@ def set_component_value(self, component_id: str, value: Any): """ self.annotation_adapter.set_component_value(component_id, value) + self._set_component_called = True return self diff --git a/tests/integration/items/test_item_context.py b/tests/integration/items/test_item_context.py index 454e4879..7bcd7ede 100644 --- a/tests/integration/items/test_item_context.py +++ b/tests/integration/items/test_item_context.py @@ -1,8 +1,12 @@ import json import os from pathlib import Path +from unittest import TestCase +from unittest.mock import MagicMock +from unittest.mock import patch from src.superannotate import FileChangedError +from src.superannotate import ItemContext from src.superannotate import SAClient from tests.integration.base import BaseTestCase @@ -135,3 +139,63 @@ def tearDown(self) -> None: sa.delete_project(self.PROJECT_NAME) except Exception: ... + + +class TestItemContextSetComponentCalledFlag(TestCase): + def _make_context(self): + ic = ItemContext( + controller=MagicMock(), + project=MagicMock(), + folder=MagicMock(), + item=MagicMock(), + overwrite=True, + ) + ic._annotation_adapter = MagicMock() + ic._annotation_adapter.annotation = {"metadata": {}, "data": {}} + return ic + + def test_dirty_flag_initial_state(self): + ic = self._make_context() + self.assertFalse(ic._set_component_called) + + def test_set_component_value_marks_dirty(self): + ic = self._make_context() + ic.set_component_value("component_id", "value") + self.assertTrue(ic._set_component_called) + + def test_save_called_on_exit_after_set_component_value(self): + ic = self._make_context() + with patch.object(ItemContext, "save", autospec=True) as save_mock: + with ic: + ic.set_component_value("component_id", "value") + save_mock.assert_called_once_with(ic) + + def test_dirty_flag_reset_after_save(self): + ic = self._make_context() + with patch.object(ic, "_set_small_annotation_adapter"), patch.object( + ic, "_set_large_annotation_adapter" + ): + ic.set_component_value("component_id", "value") + self.assertTrue(ic._set_component_called) + ic.save() + self.assertFalse(ic._set_component_called) + + def test_no_double_save_on_exit_after_manual_save(self): + ic = self._make_context() + with patch.object(ic, "_set_small_annotation_adapter"), patch.object( + ic, "_set_large_annotation_adapter" + ): + with ic: + ic.set_component_value("component_id", "value") + ic.save() + self.assertEqual(ic._annotation_adapter.save.call_count, 1) + self.assertEqual(ic._annotation_adapter.save.call_count, 1) + + def test_save_not_called_when_exception_raised(self): + ic = self._make_context() + with patch.object(ItemContext, "save", autospec=True) as save_mock: + with self.assertRaises(RuntimeError): + with ic: + ic.set_component_value("component_id", "value") + raise RuntimeError("boom") + save_mock.assert_not_called()