From 379f462d2a902804580bc979a5bd105531ff87d4 Mon Sep 17 00:00:00 2001 From: jean-robin medori Date: Mon, 25 Nov 2024 09:57:50 +0100 Subject: [PATCH] Increase test coverage --- taipy/gui_core/_context.py | 18 +- tests/gui_core/test_context_is_editable.py | 14 +- tests/gui_core/test_context_is_readable.py | 14 +- tests/gui_core/test_context_on_file_action.py | 209 ++++++++++ .../test_context_tabular_data_edit.py | 392 ++++++++++++++++++ tests/gui_core/test_context_update_data.py | 212 ++++++++++ 6 files changed, 830 insertions(+), 29 deletions(-) create mode 100644 tests/gui_core/test_context_on_file_action.py create mode 100644 tests/gui_core/test_context_tabular_data_edit.py create mode 100644 tests/gui_core/test_context_update_data.py diff --git a/taipy/gui_core/_context.py b/taipy/gui_core/_context.py index 53769a78df..2ef0d9e3f0 100644 --- a/taipy/gui_core/_context.py +++ b/taipy/gui_core/_context.py @@ -1015,10 +1015,10 @@ def get_data_node_history(self, id: str): def __check_readable_editable(self, state: State, id: str, ent_type: str, var: t.Optional[str]): if not (reason := is_readable(t.cast(ScenarioId, id))): - _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not readable: {_get_reason(reason)}.") + _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not readable: {_get_reason(reason)}") return False if not (reason := is_editable(t.cast(ScenarioId, id))): - _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not editable: {_get_reason(reason)}.") + _GuiCoreContext.__assign_var(state, var, f"{ent_type} {id} is not editable: {_get_reason(reason)}") return False return True @@ -1028,7 +1028,7 @@ def update_data(self, state: State, id: str, payload: t.Dict[str, str]): if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict): return data = t.cast(dict, args[0]) - error_var = payload.get("error_id") + error_var = data.get("error_id") entity_id = t.cast(str, data.get(_GuiCoreContext.__PROP_ENTITY_ID)) if not self.__check_readable_editable(state, entity_id, "Data node", error_var): return @@ -1130,7 +1130,9 @@ def tabular_data_edit(self, state: State, var_name: str, payload: dict): # noqa "Error updating data node tabular value: type does not support at[] indexer.", ) if new_data is not None: - datanode.write(new_data, comment=user_data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT)) + datanode.write(new_data, + editor_id=self.gui._get_client_id(), + comment=user_data.get(_GuiCoreContext.__PROP_ENTITY_COMMENT)) _GuiCoreContext.__assign_var(state, error_var, "") except Exception as e: _GuiCoreContext.__assign_var(state, error_var, f"Error updating data node tabular value. {e}") @@ -1217,6 +1219,8 @@ def on_dag_select(self, state: State, id: str, payload: t.Dict[str, str]): def on_file_action(self, state: State, id: str, payload: t.Dict[str, t.Any]): args = t.cast(list, payload.get("args")) + if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict): + return act_payload = t.cast(t.Dict[str, str], args[0]) dn_id = t.cast(DataNodeId, act_payload.get("id")) error_id = act_payload.get("error_id", "") @@ -1224,11 +1228,10 @@ def on_file_action(self, state: State, id: str, payload: t.Dict[str, t.Any]): try: dn = t.cast(_FileDataNodeMixin, core_get(dn_id)) if act_payload.get("action") == "export": - path = dn._get_downloadable_path() - if path: + if reason := dn.is_downloadable(): + path = dn._get_downloadable_path() self.gui._download(Path(path), dn_id) else: - reason = dn.is_downloadable() state.assign( error_id, "Data unavailable: " @@ -1242,6 +1245,7 @@ def on_file_action(self, state: State, id: str, payload: t.Dict[str, t.Any]): act_payload.get("path", ""), t.cast(t.Callable[[str, t.Any], bool], checker) if callable(checker) else None, editor_id=self.gui._get_client_id(), + comment=None ) ): state.assign(error_id, f"Data unavailable: {reason.reasons}") diff --git a/tests/gui_core/test_context_is_editable.py b/tests/gui_core/test_context_is_editable.py index 623d83fbc9..bc0f9a6161 100644 --- a/tests/gui_core/test_context_is_editable.py +++ b/tests/gui_core/test_context_is_editable.py @@ -254,10 +254,9 @@ def test_update_data(self): MockState(assign=assign), "", { - "args": [ - {"id": a_datanode.id}, - ], - "error_id": "error_var", + "args": [{ + "id": a_datanode.id, + "error_id": "error_var"}], }, ) assign.assert_called() @@ -269,12 +268,7 @@ def test_update_data(self): gui_core_context.update_data( MockState(assign=assign), "", - { - "args": [ - {"id": a_datanode.id}, - ], - "error_id": "error_var", - }, + {"args": [{"id": a_datanode.id, "error_id": "error_var"}]}, ) assign.assert_called_once() assert assign.call_args.args[0] == "error_var" diff --git a/tests/gui_core/test_context_is_readable.py b/tests/gui_core/test_context_is_readable.py index 29242610ec..db80af5ed8 100644 --- a/tests/gui_core/test_context_is_readable.py +++ b/tests/gui_core/test_context_is_readable.py @@ -395,12 +395,7 @@ def test_update_data(self): gui_core_context.update_data( MockState(assign=assign), "", - { - "args": [ - {"id": a_datanode.id}, - ], - "error_id": "error_var", - }, + {"args": [{"id": a_datanode.id, "error_id": "error_var"}]}, ) assign.assert_called() assert assign.call_args_list[0].args[0] == "error_var" @@ -411,12 +406,7 @@ def test_update_data(self): gui_core_context.update_data( MockState(assign=assign), "", - { - "args": [ - {"id": a_datanode.id}, - ], - "error_id": "error_var", - }, + {"args": [{"id": a_datanode.id, "error_id": "error_var"}]}, ) assign.assert_called_once() assert assign.call_args.args[0] == "error_var" diff --git a/tests/gui_core/test_context_on_file_action.py b/tests/gui_core/test_context_on_file_action.py new file mode 100644 index 0000000000..3605532734 --- /dev/null +++ b/tests/gui_core/test_context_on_file_action.py @@ -0,0 +1,209 @@ +# Copyright 2021-2024 Avaiga Private Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +import typing as t +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +from taipy import DataNode, Gui, Scope +from taipy.core.data import PickleDataNode +from taipy.core.data._data_manager_factory import _DataManagerFactory +from taipy.core.data._file_datanode_mixin import _FileDataNodeMixin +from taipy.core.reason import Reason, ReasonCollection +from taipy.gui_core._context import _GuiCoreContext + +dn = PickleDataNode("dn_config_id", + scope = Scope.GLOBAL, + properties={"default_path": "pa/th"}) + +def core_get(entity_id): + if entity_id == dn.id: + return dn + return None + + +def not_downloadable (): + return ReasonCollection()._add_reason(dn.id, Reason("foo")) + + +def downloadable(): + return ReasonCollection() + + +def not_readable(entity_id): + return ReasonCollection()._add_reason(entity_id, Reason("foo")) + + +def readable(entity_id): + return ReasonCollection() + + +def mock_checker(**kwargs): + return True + + +def check_fails(**kwargs): + raise Exception("Failed") + + +def upload_fails (a, b, editor_id, comment): + return ReasonCollection()._add_reason(dn.id, Reason("bar")) + + +def download_fails (a, b, editor_id, comment): + return ReasonCollection()._add_reason(dn.id, Reason("bar")) + + +class MockState: + def __init__(self, **kwargs) -> None: + self.assign = kwargs.get("assign") + + +class TestGuiCoreContext_on_file_action: + + @pytest.fixture(scope="class", autouse=True) + def set_entities(self): + _DataManagerFactory._build_manager()._set(dn) + + def test_does_not_fail_if_wrong_args(self): + gui_core_context = _GuiCoreContext(Mock(Gui)) + gui_core_context.on_file_action(state=Mock(), id="", payload={}) + gui_core_context.on_file_action(state=Mock(), id="", payload={"args": "wrong_args"}) + gui_core_context.on_file_action(state=Mock(), id="", payload={"args": ["wrong_args"]}) + + def test_datanode_not_readable(self): + with patch("taipy.gui_core._context.is_readable", side_effect=not_readable): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.on_file_action( + state=MockState(assign=assign), + id="", + payload={"args": [{"id": dn.id, "error_id": "error_var"}]}, + ) + mock_core_get.assert_not_called() + mock_write.assert_not_called() + assign.assert_called_once_with("error_var", "foo.") + + def test_upload_file_without_checker(self): + with patch("taipy.gui_core._context.is_readable", side_effect=readable): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(_FileDataNodeMixin, "_upload") as mock_upload: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.on_file_action( + state=MockState(assign=assign), + id="", + payload={"args": [{"id": dn.id, "error_id": "error_var", "path": "pa/th"}]}, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_upload.assert_called_once_with( + "pa/th", + None, + editor_id="a_client_id", + comment=None) + assign.assert_not_called() + + def test_upload_file_with_checker(self): + with patch("taipy.gui_core._context.is_readable", side_effect=readable): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(_FileDataNodeMixin, "_upload") as mock_upload: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + mockGui._get_user_function = lambda _ : _ + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.on_file_action( + state=MockState(assign=assign), + id="", + payload={"args": [ + {"id": dn.id, "error_id": "error_var", "path": "pa/th", "upload_check": mock_checker}]}, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_upload.assert_called_once_with( + "pa/th", + t.cast(t.Callable[[str, t.Any], bool], mock_checker), + editor_id="a_client_id", + comment=None) + assign.assert_not_called() + + def test_upload_file_with_failing_checker(self): + with patch("taipy.gui_core._context.is_readable", side_effect=readable): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(_FileDataNodeMixin, "_upload", side_effect=upload_fails) as mock_upload: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + mockGui._get_user_function = lambda _ : _ + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.on_file_action( + state=MockState(assign=assign), + id="", + payload={"args": [ + {"id": dn.id, "error_id": "error_var", "path": "pa/th", "upload_check": check_fails}]}, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_upload.assert_called_once_with( + "pa/th", + t.cast(t.Callable[[str, t.Any], bool], check_fails), + editor_id="a_client_id", + comment=None) + assign.assert_called_once_with("error_var", "Data unavailable: bar.") + + def test_download_file_not_downloadable(self): + with patch.object(_FileDataNodeMixin, "is_downloadable", side_effect=not_downloadable): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(_FileDataNodeMixin, "_get_downloadable_path") as mock_download: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + mockGui._get_user_function = lambda _ : _ + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.on_file_action( + state=MockState(assign=assign), + id="", + payload={"args": [ + {"id": dn.id, + "action": "export", + "error_id": "error_var"}]}, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_download.assert_not_called() + assign.assert_called_once_with("error_var", "Data unavailable: foo.") + + def test_download(self): + with patch.object(_FileDataNodeMixin, "is_downloadable", side_effect=downloadable): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(_FileDataNodeMixin, "_get_downloadable_path") as mock_download: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + mockGui._download.return_value = None + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.on_file_action( + state=MockState(assign=assign), + id="", + payload={"args": [ + {"id": dn.id, + "action": "export", + "error_id": "error_var"}]}, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_download.assert_called_once() + mockGui._download.assert_called_once_with(Path(dn._get_downloadable_path()), dn.id) + assign.assert_not_called() diff --git a/tests/gui_core/test_context_tabular_data_edit.py b/tests/gui_core/test_context_tabular_data_edit.py new file mode 100644 index 0000000000..017fe5dbbe --- /dev/null +++ b/tests/gui_core/test_context_tabular_data_edit.py @@ -0,0 +1,392 @@ +# Copyright 2021-2024 Avaiga Private Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from unittest.mock import Mock, patch + +import pandas as pd + +from taipy import DataNode, Gui +from taipy.common.config.common.scope import Scope +from taipy.core.data._data_manager_factory import _DataManagerFactory +from taipy.core.data.pickle import PickleDataNode +from taipy.core.reason import Reason, ReasonCollection +from taipy.gui_core._context import _GuiCoreContext + +dn = PickleDataNode("dn_config_id", scope = Scope.GLOBAL) + + +def core_get(entity_id): + if entity_id == dn.id: + return dn + return None + + +def is_false(entity_id): + return ReasonCollection()._add_reason(entity_id, Reason("foo")) + + +def is_true(entity_id): + return True + +def fails(**kwargs): + raise Exception("Failed") + + +class MockState: + def __init__(self, **kwargs) -> None: + self.assign = kwargs.get("assign") + + +class TestGuiCoreContext_update_data: + + def test_do_not_edit_tabular_data_if_not_readable(self): + _DataManagerFactory._build_manager()._set(dn) + with patch("taipy.gui_core._context.is_readable", side_effect=is_false): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data() + + mock_core_get.assert_not_called() + mock_write.assert_not_called() + assign.assert_called_once_with("error_var", f"Data node {dn.id} is not readable: foo.") + + def test_do_not_edit_tabular_data_if_not_editable(self): + _DataManagerFactory._build_manager()._set(dn) + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_false): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data() + + mock_core_get.assert_not_called() + mock_write.assert_not_called() + assign.assert_called_once_with("error_var", f"Data node {dn.id} is not editable: foo.") + + def test_edit_pandas_data(self): + dn.write(pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})) + idx = 0 + col = "a" + new_value = 100 + new_data = pd.DataFrame({"a": [new_value, 2, 3], "b": [4, 5, 6]}) + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once() + # Cannot use the following line because of the pandas DataFrame comparison + # mock_write.assert_called_once_with(new_data, editor_id="a_client_id", comment=None + # Instead, we will compare the arguments of the call manually + assert mock_write.call_args_list[0].args[0].equals(new_data) + assert mock_write.call_args_list[0].kwargs["editor_id"] == "a_client_id" + assert mock_write.call_args_list[0].kwargs["comment"] is None + assign.assert_called_once_with("error_var", "") + + def __call_update_data(self, col=None, idx=None, new_value=None): + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + payload = {"user_data": {"dn_id": dn.id}, "error_id": "error_var"} + if idx is not None: + payload["index"] = idx + if col is not None: + payload["col"] = col + if new_value is not None: + payload["value"] = new_value + assign = Mock() + gui_core_context.tabular_data_edit( + state=MockState(assign=assign), + var_name="", + payload=payload, + ) + return assign + + def test_edit_pandas_wrong_idx(self): + data = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + dn.write(data) + idx = 5 + col = "a" + new_value = 100 + new_data = data.copy() + new_data.at[idx, col] = new_value + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + + assign = self.__call_update_data(col, idx, new_value) + + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once() # TODO should not been called + # Cannot use the following line because of the pandas DataFrame comparison + # mock_write.assert_called_once_with(new_data, editor_id="a_client_id", comment=None + # Instead, we will compare the arguments of the call manually + assert mock_write.call_args_list[0].args[0].equals(new_data) + assert mock_write.call_args_list[0].kwargs["editor_id"] == "a_client_id" + assert mock_write.call_args_list[0].kwargs["comment"] is None + assign.assert_called_once_with("error_var", "") # TODO have a message + + def test_edit_pandas_wrong_col(self): + data = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + dn.write(data) + idx = 0 + col = "c" + new_value = 100 + new_data = data.copy() + new_data.at[idx, col] = new_value + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once() # TODO should not been called + # Cannot use the following line because of the pandas DataFrame comparison + # mock_write.assert_called_once_with(new_data, editor_id="a_client_id", comment=None + # Instead, we will compare the arguments of the call manually + assert mock_write.call_args_list[0].args[0].equals(new_data) + assert mock_write.call_args_list[0].kwargs["editor_id"] == "a_client_id" + assert mock_write.call_args_list[0].kwargs["comment"] is None + assign.assert_called_once_with("error_var", "") # TODO should have a message + + def test_edit_pandas_series(self): + data = pd.Series([1, 2, 3]) + dn.write(data) + idx = 0 + col = "WHATEVER" + new_value = 100 + new_data = pd.Series([100, 2, 3]) + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once() + # Cannot use the following line because of the pandas Series comparison + # mock_write.assert_called_once_with(new_data, editor_id="a_client_id", comment=None + # Instead, we will compare the arguments of the call manually + assert mock_write.call_args_list[0].args[0].equals(new_data) + assert mock_write.call_args_list[0].kwargs["editor_id"] == "a_client_id" + assert mock_write.call_args_list[0].kwargs["comment"] is None + assign.assert_called_once_with("error_var", "") + + def test_edit_pandas_series_wrong_idx(self): + data = pd.Series([1, 2, 3]) + dn.write(data) + idx = 5 + col = "WHATEVER" + new_value = 100 + new_data = data.copy() + new_data.at[idx] = new_value + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once() # TODO should not been called + # Cannot use the following line because of the pandas Series comparison + # mock_write.assert_called_once_with(new_data, editor_id="a_client_id", comment=None + # Instead, we will compare the arguments of the call manually + assert mock_write.call_args_list[0].args[0].equals(new_data) + assert mock_write.call_args_list[0].kwargs["editor_id"] == "a_client_id" + assert mock_write.call_args_list[0].kwargs["comment"] is None + assign.assert_called_once_with("error_var", "") # TODO should have a message + + def test_edit_dict(self): + data = {"a": [1, 2, 3], "b": [4, 5, 6]} + dn.write(data) + idx = 0 + col = "a" + new_value = 100 + new_data = {"a": [100, 2, 3], "b": [4, 5, 6]} + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once_with(new_data, editor_id="a_client_id", comment=None) + assign.assert_called_once_with("error_var", "") + + def test_edit_dict_wrong_idx(self): + data = {"a": [1, 2, 3], "b": [4, 5, 6]} + dn.write(data) + idx = 5 + col = "a" + new_value = 100 + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_not_called() + assign.assert_called_once_with( + "error_var", + "Error updating data node tabular value. list assignment index out of range") # TODO + # message unclear + + def test_edit_dict_wrong_col(self): + data = {"a": [1, 2, 3], "b": [4, 5, 6]} + dn.write(data) + idx = 0 + col = "c" + new_value = 100 + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_not_called() + assign.assert_called_once_with( + "error_var", + "Error updating Data node: dict values must be list or tuple.") # TODO Wrong message + + def test_edit_dict_of_tuples(self): + data = {"a": (1, 2, 3), "b": (4, 5, 6)} + dn.write(data) + idx = 0 + col = "a" + new_value = 100 + new_data = {"a": (100, 2, 3), "b": (4, 5, 6)} + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once_with(new_data, editor_id="a_client_id", comment=None) + assign.assert_called_once_with("error_var", "") + + def test_edit_dict_of_tuples_wrong_idx(self): + data = {"a": (1, 2, 3), "b": (4, 5, 6)} + dn.write(data) + idx = 5 + col = "a" + new_value = 100 + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_not_called() + assign.assert_called_once_with( + "error_var", + "Error updating data node tabular value. list assignment index out of range") # TODO + # message unclear + + def test_edit_dict_of_tuples_wrong_col(self): + data = {"a": (1, 2, 3), "b": (4, 5, 6)} + dn.write(data) + idx = 0 + col = "c" + new_value = 100 + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_not_called() + assign.assert_called_once_with( + "error_var", + "Error updating Data node: dict values must be list or tuple.") # TODO Wrong message + + def test_edit_wrong_dict(self): + data = {"a": 1, "b": 2} + dn.write(data) + idx = 0 + col = "a" + new_value = 100 + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_not_called() + assign.assert_called_once_with( + "error_var", + "Error updating Data node: dict values must be list or tuple.") + + def test_edit_list(self): + data = [[1, 2, 3], [4, 5, 6]] + dn.write(data) + idx = 0 + col = 1 + new_value = 100 + new_data = [[1, 100, 3], [4, 5, 6]] + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once_with(new_data, editor_id="a_client_id", comment=None) + assign.assert_called_once_with("error_var", "") + + def test_edit_list_wrong_idx(self): + data = [[1, 2, 3], [4, 5, 6]] + dn.write(data) + idx = 5 + col = 0 + new_value = 100 + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_not_called() + assign.assert_called_once_with( + "error_var", + "Error updating data node tabular value. list index out of range") # TODO + # message unclear + + def test_edit_list_wrong_col(self): + data = [[1, 2, 3], [4, 5, 6]] + dn.write(data) + idx = 0 + col = 5 + new_value = 100 + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_not_called() + assign.assert_called_once_with( + "error_var", + "Error updating data node tabular value. list assignment index out of range") # TODO + # message unclear + + def test_edit_tuple(self): + data = ([1, 2, 3], [4, 5, 6]) + dn.write(data) + idx = 0 + col = 1 + new_value = 100 + new_data = ([1, 100, 3], [4, 5, 6]) + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + assign = self.__call_update_data(col, idx, new_value) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once_with(new_data, editor_id="a_client_id", comment=None) + assign.assert_called_once_with("error_var", "") diff --git a/tests/gui_core/test_context_update_data.py b/tests/gui_core/test_context_update_data.py new file mode 100644 index 0000000000..aca8fad5de --- /dev/null +++ b/tests/gui_core/test_context_update_data.py @@ -0,0 +1,212 @@ +# Copyright 2021-2024 Avaiga Private Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +from unittest.mock import Mock, patch + +import pytest +from win32ctypes.pywin32.pywintypes import datetime + +from taipy import DataNode, Gui +from taipy.common.config.common.scope import Scope +from taipy.core.data._data_manager_factory import _DataManagerFactory +from taipy.core.data.pickle import PickleDataNode +from taipy.core.reason import Reason, ReasonCollection +from taipy.gui_core._context import _GuiCoreContext + +dn = PickleDataNode("data_node_config_id", Scope.SCENARIO) + + +def core_get(entity_id): + if entity_id == dn.id: + return dn + return None + + +def is_false(entity_id): + return ReasonCollection()._add_reason(entity_id, Reason("foo")) + + +def is_true(entity_id): + return True + +def fails(**kwargs): + raise Exception("Failed") + + +class MockState: + def __init__(self, **kwargs) -> None: + self.assign = kwargs.get("assign") + + +class TestGuiCoreContext_update_data: + + @pytest.fixture(scope="class", autouse=True) + def set_entities(self): + _DataManagerFactory._build_manager()._set(dn) + + def test_does_not_fail_if_wrong_args(self): + gui_core_context = _GuiCoreContext(Mock(Gui)) + gui_core_context.update_data(state=Mock(), id="", payload={}) + gui_core_context.update_data(state=Mock(), id="", payload={"args": "wrong_args"}) + gui_core_context.update_data(state=Mock(), id="", payload={"args": ["wrong_args"]}) + + def test_do_not_update_data_if_not_readable(self): + with patch("taipy.gui_core._context.is_readable", side_effect=is_false): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.update_data( + state=MockState(assign=assign), + id="", + payload={"args": [{"id": dn.id,"error_id": "error_var"}]}, + ) + mock_core_get.assert_not_called() + mock_write.assert_not_called() + assign.assert_called_once_with("error_var", f"Data node {dn.id} is not readable: foo.") + + def test_do_not_update_data_if_not_editable(self): + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_false): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.update_data( + state=MockState(assign=assign), + id="", + payload={"args": [{"id": dn.id,"error_id": "error_var"}]}, + ) + mock_core_get.assert_not_called() + mock_write.assert_not_called() + assign.assert_called_once_with("error_var", f"Data node {dn.id} is not editable: foo.") + + def test_write_str_data_with_editor_and_comment(self): + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.update_data( + state=MockState(assign=assign), + id="", + payload={ + "args": [{ + "id": dn.id, + "value": "data to write", + "comment": "The comment", + "error_id": "error_var"}], + }, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once_with("data to write", + editor_id="a_client_id", + comment="The comment") + assign.assert_called_once_with("error_var", "") + + def test_write_date_data_with_editor_and_comment(self): + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + date = datetime(2000, 1, 1, 0, 0, 0) + gui_core_context.update_data( + state=MockState(assign=assign), + id="", + payload={ + "args": [ + { + "id": dn.id, + "value": "2000-01-01 00:00:00", + "type": "date", + "comment": "The comment", + "error_id": "error_var" + }], + }, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once_with(date, + editor_id="a_client_id", + comment="The comment") + assign.assert_called_once_with("error_var", "") + + def test_write_int_data_with_editor_and_comment(self): + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.update_data( + state=MockState(assign=assign), + id="", + payload={ + "args": [{"id": dn.id, "value": "1", "type": "int", "error_id": "error_var"}], + }, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once_with(1, editor_id="a_client_id", comment=None) + assign.assert_called_once_with("error_var", "") + + def test_write_float_data_with_editor_and_comment(self): + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write") as mock_write: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.update_data( + state=MockState(assign=assign), + id="", + payload={ + "args": [{"id": dn.id, "value": "1.9", "type": "float", "error_id": "error_var"}], + }, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once_with(1.9, editor_id="a_client_id", comment=None) + assign.assert_called_once_with("error_var", "") + + def test_fails_and_catch_the_error(self): + with patch("taipy.gui_core._context.is_readable", side_effect=is_true): + with patch("taipy.gui_core._context.is_editable", side_effect=is_true): + with patch("taipy.gui_core._context.core_get", side_effect=core_get) as mock_core_get: + with patch.object(DataNode, "write", side_effect=fails) as mock_write: + mockGui = Mock(Gui) + mockGui._get_client_id = lambda: "a_client_id" + gui_core_context = _GuiCoreContext(mockGui) + assign = Mock() + gui_core_context.update_data( + state=MockState(assign=assign), + id="", + payload={ + "args": [{"id": dn.id, "value": "1.9", "type": "float", "error_id": "error_var"}], + }, + ) + mock_core_get.assert_called_once_with(dn.id) + mock_write.assert_called_once_with(1.9, editor_id="a_client_id", comment=None) + assign.assert_called_once() + assert assign.call_args_list[0].args[0] == "error_var" + assert "Error updating Data node value." in assign.call_args_list[0].args[1]