Преглед изворни кода

Merge pull request #1276 from Avaiga/feature/#746-creating-Reason-class

feature/#746 added reason class
Toan Quach пре 1 година
родитељ
комит
881c295a46

+ 13 - 18
taipy/core/_entity/_ready_to_run_property.py

@@ -11,6 +11,7 @@
 
 
 from typing import TYPE_CHECKING, Dict, Set, Union
 from typing import TYPE_CHECKING, Dict, Set, Union
 
 
+from ..common.reason import Reason
 from ..notification import EventOperation, Notifier, _make_event
 from ..notification import EventOperation, Notifier, _make_event
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -28,7 +29,7 @@ class _ReadyToRunProperty:
 
 
     # A nested dictionary of the submittable entities (Scenario, Sequence, Task) and
     # A nested dictionary of the submittable entities (Scenario, Sequence, Task) and
     # the data nodes that make it not ready_to_run with the reason(s)
     # the data nodes that make it not ready_to_run with the reason(s)
-    _submittable_id_datanodes: Dict[Union["ScenarioId", "SequenceId", "TaskId"], Dict["DataNodeId", Set[str]]] = {}
+    _submittable_id_datanodes: Dict[Union["ScenarioId", "SequenceId", "TaskId"], Reason] = {}
 
 
     @classmethod
     @classmethod
     def _add(cls, dn: "DataNode", reason: str) -> None:
     def _add(cls, dn: "DataNode", reason: str) -> None:
@@ -40,13 +41,13 @@ class _ReadyToRunProperty:
 
 
         for scenario_parent in parent_entities.get(Scenario._MANAGER_NAME, []):
         for scenario_parent in parent_entities.get(Scenario._MANAGER_NAME, []):
             if dn in scenario_parent.get_inputs():
             if dn in scenario_parent.get_inputs():
-                _ReadyToRunProperty.__add(scenario_parent, dn, reason)
+                cls.__add(scenario_parent, dn, reason)
         for sequence_parent in parent_entities.get(Sequence._MANAGER_NAME, []):
         for sequence_parent in parent_entities.get(Sequence._MANAGER_NAME, []):
             if dn in sequence_parent.get_inputs():
             if dn in sequence_parent.get_inputs():
-                _ReadyToRunProperty.__add(sequence_parent, dn, reason)
+                cls.__add(sequence_parent, dn, reason)
         for task_parent in parent_entities.get(Task._MANAGER_NAME, []):
         for task_parent in parent_entities.get(Task._MANAGER_NAME, []):
             if dn in task_parent.input.values():
             if dn in task_parent.input.values():
-                _ReadyToRunProperty.__add(task_parent, dn, reason)
+                cls.__add(task_parent, dn, reason)
 
 
     @classmethod
     @classmethod
     def _remove(cls, datanode: "DataNode", reason: str) -> None:
     def _remove(cls, datanode: "DataNode", reason: str) -> None:
@@ -58,18 +59,14 @@ class _ReadyToRunProperty:
         to_remove_dn = False
         to_remove_dn = False
         for submittable_id in submittable_ids:
         for submittable_id in submittable_ids:
             # check remove the reason
             # check remove the reason
-            if reason in cls._submittable_id_datanodes.get(submittable_id, {}).get(datanode.id, set()):
-                cls._submittable_id_datanodes[submittable_id][datanode.id].remove(reason)
-            if len(cls._submittable_id_datanodes.get(submittable_id, {}).get(datanode.id, set())) == 0:
-                to_remove_dn = True
-                cls._submittable_id_datanodes.get(submittable_id, {}).pop(datanode.id, None)
-                if (
-                    submittable_id in cls._submittable_id_datanodes
-                    and len(cls._submittable_id_datanodes[submittable_id]) == 0
-                ):
+            reason_entity = cls._submittable_id_datanodes.get(submittable_id)
+            if reason_entity is not None:
+                reason_entity._remove_reason(datanode.id, reason)
+                to_remove_dn = not reason_entity._entity_id_exists_in_reason(datanode.id)
+                if reason_entity:
                     submittable = tp_get(submittable_id)
                     submittable = tp_get(submittable_id)
                     cls.__publish_submittable_property_event(submittable, True)
                     cls.__publish_submittable_property_event(submittable, True)
-                    cls._submittable_id_datanodes.pop(submittable_id, None)
+                    cls._submittable_id_datanodes.pop(submittable_id)
 
 
         if to_remove_dn:
         if to_remove_dn:
             cls._datanode_id_submittables.pop(datanode.id)
             cls._datanode_id_submittables.pop(datanode.id)
@@ -84,10 +81,8 @@ class _ReadyToRunProperty:
             cls.__publish_submittable_property_event(submittable, False)
             cls.__publish_submittable_property_event(submittable, False)
 
 
         if submittable.id not in cls._submittable_id_datanodes:
         if submittable.id not in cls._submittable_id_datanodes:
-            cls._submittable_id_datanodes[submittable.id] = {}
-        if datanode.id not in cls._submittable_id_datanodes[submittable.id]:
-            cls._submittable_id_datanodes[submittable.id][datanode.id] = set()
-        cls._submittable_id_datanodes[submittable.id][datanode.id].add(reason)
+            cls._submittable_id_datanodes[submittable.id] = Reason(submittable.id)
+        cls._submittable_id_datanodes[submittable.id]._add_reason(datanode.id, reason)
 
 
     @staticmethod
     @staticmethod
     def __publish_submittable_property_event(
     def __publish_submittable_property_event(

+ 13 - 3
taipy/core/_entity/submittable.py

@@ -17,6 +17,7 @@ import networkx as nx
 
 
 from ..common._listattributes import _ListAttributes
 from ..common._listattributes import _ListAttributes
 from ..common._utils import _Subscriber
 from ..common._utils import _Subscriber
+from ..common.reason import Reason
 from ..data.data_node import DataNode
 from ..data.data_node import DataNode
 from ..job.job import Job
 from ..job.job import Job
 from ..submission.submission import Submission
 from ..submission.submission import Submission
@@ -81,13 +82,22 @@ class Submittable:
         all_data_nodes_in_dag = {node for node in dag.nodes if isinstance(node, DataNode)}
         all_data_nodes_in_dag = {node for node in dag.nodes if isinstance(node, DataNode)}
         return all_data_nodes_in_dag - self.__get_inputs(dag) - self.__get_outputs(dag)
         return all_data_nodes_in_dag - self.__get_inputs(dag) - self.__get_outputs(dag)
 
 
-    def is_ready_to_run(self) -> bool:
+    def is_ready_to_run(self) -> Reason:
         """Indicate if the entity is ready to be run.
         """Indicate if the entity is ready to be run.
 
 
         Returns:
         Returns:
-            True if the given entity is ready to be run. False otherwise.
+            A Reason object that can function as a Boolean value.
+            which is True if the given entity is ready to be run or there is no reason to be blocked, False otherwise.
         """
         """
-        return all(dn.is_ready_for_reading for dn in self.get_inputs())
+        reason = Reason(self._submittable_id)
+
+        for node in self.get_inputs():
+            if node._edit_in_progress:
+                reason._add_reason(node.id, node._build_edit_in_progress_reason())
+            if not node._last_edit_date:
+                reason._add_reason(node.id, node._build_not_written_reason())
+
+        return reason
 
 
     def data_nodes_being_edited(self) -> Set[DataNode]:
     def data_nodes_being_edited(self) -> Set[DataNode]:
         """Return the set of data nodes of the submittable entity that are being edited.
         """Return the set of data nodes of the submittable entity that are being edited.

+ 4 - 0
taipy/core/_manager/_manager.py

@@ -27,6 +27,10 @@ class _Manager(Generic[EntityType]):
     _logger = _TaipyLogger._get_logger()
     _logger = _TaipyLogger._get_logger()
     _ENTITY_NAME: str = "Entity"
     _ENTITY_NAME: str = "Entity"
 
 
+    @classmethod
+    def _build_not_submittable_entity_reason(cls, entity_id: str) -> str:
+        return f"Entity {entity_id} is not a submittable entity"
+
     @classmethod
     @classmethod
     def _delete_all(cls):
     def _delete_all(cls):
         """
         """

+ 41 - 0
taipy/core/common/reason.py

@@ -0,0 +1,41 @@
+# 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 typing import Dict, Set
+
+
+class Reason:
+    def __init__(self, entity_id: str) -> None:
+        self.entity_id: str = entity_id
+        self._reasons: Dict[str, Set[str]] = {}
+
+    def _add_reason(self, entity_id: str, reason: str) -> "Reason":
+        if entity_id not in self._reasons:
+            self._reasons[entity_id] = set()
+        self._reasons[entity_id].add(reason)
+        return self
+
+    def _remove_reason(self, entity_id: str, reason: str) -> "Reason":
+        if entity_id in self._reasons and reason in self._reasons[entity_id]:
+            self._reasons[entity_id].remove(reason)
+            if len(self._reasons[entity_id]) == 0:
+                del self._reasons[entity_id]
+        return self
+
+    def _entity_id_exists_in_reason(self, entity_id: str) -> bool:
+        return entity_id in self._reasons
+
+    def __bool__(self) -> bool:
+        return len(self._reasons) == 0
+
+    @property
+    def reasons(self) -> str:
+        return "; ".join("; ".join(reason) for reason in self._reasons.values()) + "." if self._reasons else ""

+ 6 - 0
taipy/core/data/data_node.py

@@ -227,6 +227,9 @@ class DataNode(_Entity, _Labeled):
     def last_edit_date(self, val):
     def last_edit_date(self, val):
         self._last_edit_date = val
         self._last_edit_date = val
 
 
+    def _build_not_written_reason(self) -> str:
+        return f"DataNode {self.id} is not written"
+
     @property  # type: ignore
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     @_self_reload(_MANAGER_NAME)
     def scope(self):
     def scope(self):
@@ -292,6 +295,9 @@ class DataNode(_Entity, _Labeled):
     def edit_in_progress(self, val):
     def edit_in_progress(self, val):
         self._edit_in_progress = val
         self._edit_in_progress = val
 
 
+    def _build_edit_in_progress_reason(self) -> str:
+        return f"DataNode {self.id} is being edited"
+
     @property  # type: ignore
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     @_self_reload(_MANAGER_NAME)
     def editor_id(self):
     def editor_id(self):

+ 10 - 2
taipy/core/scenario/_scenario_manager.py

@@ -23,6 +23,7 @@ from .._manager._manager import _Manager
 from .._repository._abstract_repository import _AbstractRepository
 from .._repository._abstract_repository import _AbstractRepository
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_mixin import _VersionMixin
 from .._version._version_mixin import _VersionMixin
+from ..common.reason import Reason
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..config.scenario_config import ScenarioConfig
 from ..config.scenario_config import ScenarioConfig
 from ..cycle._cycle_manager_factory import _CycleManagerFactory
 from ..cycle._cycle_manager_factory import _CycleManagerFactory
@@ -199,10 +200,17 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return scenario
         return scenario
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> bool:
+    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> Reason:
         if isinstance(scenario, str):
         if isinstance(scenario, str):
             scenario = cls._get(scenario)
             scenario = cls._get(scenario)
-        return isinstance(scenario, Scenario) and scenario.is_ready_to_run()
+
+        if not isinstance(scenario, Scenario):
+            scenario = str(scenario)
+            reason = Reason((scenario))
+            reason._add_reason(scenario, cls._build_not_submittable_entity_reason(scenario))
+            return reason
+
+        return scenario.is_ready_to_run()
 
 
     @classmethod
     @classmethod
     def _submit(
     def _submit(

+ 10 - 2
taipy/core/sequence/_sequence_manager.py

@@ -18,6 +18,7 @@ from .._entity._entity_ids import _EntityIds
 from .._manager._manager import _Manager
 from .._manager._manager import _Manager
 from .._version._version_mixin import _VersionMixin
 from .._version._version_mixin import _VersionMixin
 from ..common._utils import _Subscriber
 from ..common._utils import _Subscriber
+from ..common.reason import Reason
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..exceptions.exceptions import (
 from ..exceptions.exceptions import (
     InvalidSequenceId,
     InvalidSequenceId,
@@ -296,10 +297,17 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         Notifier.publish(_make_event(sequence, EventOperation.UPDATE, attribute_name="subscribers"))
         Notifier.publish(_make_event(sequence, EventOperation.UPDATE, attribute_name="subscribers"))
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> bool:
+    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> Reason:
         if isinstance(sequence, str):
         if isinstance(sequence, str):
             sequence = cls._get(sequence)
             sequence = cls._get(sequence)
-        return isinstance(sequence, Sequence) and sequence.is_ready_to_run()
+
+        if not isinstance(sequence, Sequence):
+            sequence = str(sequence)
+            reason = Reason(sequence)
+            reason._add_reason(sequence, cls._build_not_submittable_entity_reason(sequence))
+            return reason
+
+        return sequence.is_ready_to_run()
 
 
     @classmethod
     @classmethod
     def _submit(
     def _submit(

+ 3 - 2
taipy/core/taipy.py

@@ -33,6 +33,7 @@ from .common._check_instance import (
     _is_task,
     _is_task,
 )
 )
 from .common._warnings import _warn_deprecated, _warn_no_core_service
 from .common._warnings import _warn_deprecated, _warn_no_core_service
+from .common.reason import Reason
 from .config.data_node_config import DataNodeConfig
 from .config.data_node_config import DataNodeConfig
 from .config.scenario_config import ScenarioConfig
 from .config.scenario_config import ScenarioConfig
 from .cycle._cycle_manager_factory import _CycleManagerFactory
 from .cycle._cycle_manager_factory import _CycleManagerFactory
@@ -89,7 +90,7 @@ def set(entity: Union[DataNode, Task, Sequence, Scenario, Cycle, Submission]):
         return _SubmissionManagerFactory._build_manager()._set(entity)
         return _SubmissionManagerFactory._build_manager()._set(entity)
 
 
 
 
-def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Task, TaskId, str]) -> bool:
+def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Task, TaskId, str]) -> Reason:
     """Indicate if an entity can be submitted.
     """Indicate if an entity can be submitted.
 
 
     This function checks if the given entity can be submitted for execution.
     This function checks if the given entity can be submitted for execution.
@@ -109,7 +110,7 @@ def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Tas
         return _TaskManagerFactory._build_manager()._is_submittable(entity)
         return _TaskManagerFactory._build_manager()._is_submittable(entity)
     if isinstance(entity, str) and entity.startswith(Task._ID_PREFIX):
     if isinstance(entity, str) and entity.startswith(Task._ID_PREFIX):
         return _TaskManagerFactory._build_manager()._is_submittable(TaskId(entity))
         return _TaskManagerFactory._build_manager()._is_submittable(TaskId(entity))
-    return False
+    return Reason(str(entity))._add_reason(str(entity), _Manager._build_not_submittable_entity_reason(str(entity)))
 
 
 
 
 def is_editable(
 def is_editable(

+ 17 - 2
taipy/core/task/_task_manager.py

@@ -20,6 +20,7 @@ from .._orchestrator._abstract_orchestrator import _AbstractOrchestrator
 from .._repository._abstract_repository import _AbstractRepository
 from .._repository._abstract_repository import _AbstractRepository
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_mixin import _VersionMixin
 from .._version._version_mixin import _VersionMixin
+from ..common.reason import Reason
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..config.task_config import TaskConfig
 from ..config.task_config import TaskConfig
 from ..cycle.cycle_id import CycleId
 from ..cycle.cycle_id import CycleId
@@ -163,10 +164,24 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return entity_ids
         return entity_ids
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, task: Union[Task, TaskId]) -> bool:
+    def _is_submittable(cls, task: Union[Task, TaskId]) -> Reason:
         if isinstance(task, str):
         if isinstance(task, str):
             task = cls._get(task)
             task = cls._get(task)
-        return isinstance(task, Task) and all(input_dn.is_ready_for_reading for input_dn in task.input.values())
+        if not isinstance(task, Task):
+            task = str(task)
+            reason = Reason(task)
+            reason._add_reason(task, cls._build_not_submittable_entity_reason(task))
+        else:
+            reason = Reason(task.id)
+            data_manager = _DataManagerFactory._build_manager()
+            for node in task.input.values():
+                node = data_manager._get(node)
+                if node._edit_in_progress:
+                    reason._add_reason(node.id, node._build_edit_in_progress_reason())
+                if not node._last_edit_date:
+                    reason._add_reason(node.id, node._build_not_written_reason())
+
+        return reason
 
 
     @classmethod
     @classmethod
     def _submit(
     def _submit(

+ 34 - 14
tests/core/_entity/test_ready_to_run_property.py

@@ -14,6 +14,7 @@ from taipy import ScenarioId, SequenceId, TaskId
 from taipy.config.common.frequency import Frequency
 from taipy.config.common.frequency import Frequency
 from taipy.config.config import Config
 from taipy.config.config import Config
 from taipy.core._entity._ready_to_run_property import _ReadyToRunProperty
 from taipy.core._entity._ready_to_run_property import _ReadyToRunProperty
+from taipy.core.common.reason import Reason
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
 from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
 from taipy.core.task._task_manager_factory import _TaskManagerFactory
 from taipy.core.task._task_manager_factory import _TaskManagerFactory
@@ -32,6 +33,7 @@ def test_scenario_without_input_is_ready_to_run():
     scenario = scenario_manager._create(scenario_config)
     scenario = scenario_manager._create(scenario_config)
 
 
     assert scenario_manager._is_submittable(scenario)
     assert scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
 
 
@@ -44,6 +46,7 @@ def test_scenario_submittable_with_inputs_is_ready_to_run():
     scenario = scenario_manager._create(scenario_config)
     scenario = scenario_manager._create(scenario_config)
 
 
     assert scenario_manager._is_submittable(scenario)
     assert scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
 
 
@@ -58,7 +61,7 @@ def test_scenario_submittable_even_with_output_not_ready_to_run():
     dn_3 = scenario.dn_3
     dn_3 = scenario.dn_3
 
 
     assert not dn_3.is_ready_for_reading
     assert not dn_3.is_ready_for_reading
-    assert scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
 
 
@@ -75,6 +78,7 @@ def test_scenario_not_submittable_not_in_property_because_it_is_lazy():
     assert dn_1.is_ready_for_reading
     assert dn_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     assert not scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
 
 
     # Since it is a lazy property, the scenario and the datanodes is not yet in the dictionary
     # Since it is a lazy property, the scenario and the datanodes is not yet in the dictionary
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
@@ -93,14 +97,16 @@ def test_scenario_not_submittable_if_one_input_edit_in_progress():
 
 
     assert not dn_1.is_ready_for_reading
     assert not dn_1.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     assert not scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
 
 
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
-    assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]
+    assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
     assert dn_1.id in _ReadyToRunProperty._datanode_id_submittables
     assert dn_1.id in _ReadyToRunProperty._datanode_id_submittables
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
-    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id][dn_1.id] == {
+    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
         f"DataNode {dn_1.id} is being edited"
         f"DataNode {dn_1.id} is being edited"
     }
     }
+    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons == f"DataNode {dn_1.id} is being edited."
 
 
 
 
 def test_scenario_not_submittable_for_multiple_reasons():
 def test_scenario_not_submittable_for_multiple_reasons():
@@ -119,21 +125,25 @@ def test_scenario_not_submittable_for_multiple_reasons():
     assert not dn_1.is_ready_for_reading
     assert not dn_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     assert not scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
 
 
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
-    assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]
-    assert dn_2.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]
+    assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
+    assert dn_2.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
     assert dn_1.id in _ReadyToRunProperty._datanode_id_submittables
     assert dn_1.id in _ReadyToRunProperty._datanode_id_submittables
     assert dn_2.id in _ReadyToRunProperty._datanode_id_submittables
     assert dn_2.id in _ReadyToRunProperty._datanode_id_submittables
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
-    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id][dn_1.id] == {
+    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
         f"DataNode {dn_1.id} is being edited"
         f"DataNode {dn_1.id} is being edited"
     }
     }
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_2.id]
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_2.id]
-    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id][dn_2.id] == {
+    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_2.id] == {
         f"DataNode {dn_2.id} is being edited",
         f"DataNode {dn_2.id} is being edited",
         f"DataNode {dn_2.id} is not written",
         f"DataNode {dn_2.id} is not written",
     }
     }
+    reason_str = _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons
+    assert f"DataNode {dn_2.id} is being edited" in reason_str
+    assert f"DataNode {dn_2.id} is not written" in reason_str
 
 
 
 
 def test_writing_input_remove_reasons():
 def test_writing_input_remove_reasons():
@@ -146,17 +156,22 @@ def test_writing_input_remove_reasons():
 
 
     assert not dn_1.is_ready_for_reading
     assert not dn_1.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
     assert not scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
     # Since it is a lazy property, the scenario is not yet in the dictionary
     # Since it is a lazy property, the scenario is not yet in the dictionary
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
     dn_1.lock_edit()
     dn_1.lock_edit()
-    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id][dn_1.id] == {
+    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
         f"DataNode {dn_1.id} is being edited",
         f"DataNode {dn_1.id} is being edited",
         f"DataNode {dn_1.id} is not written",
         f"DataNode {dn_1.id} is not written",
     }
     }
+    reason_str = _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons
+    assert f"DataNode {dn_1.id} is being edited" in reason_str
+    assert f"DataNode {dn_1.id} is not written" in reason_str
 
 
     dn_1.write(10)
     dn_1.write(10)
     assert scenario_manager._is_submittable(scenario)
     assert scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id not in _ReadyToRunProperty._datanode_id_submittables
     assert dn_1.id not in _ReadyToRunProperty._datanode_id_submittables
 
 
@@ -165,20 +180,25 @@ def identity(arg):
     return arg
     return arg
 
 
 
 
-def __assert_not_submittable_becomes_submittable_when_dn_edited(sequence, manager, dn):
+def __assert_not_submittable_becomes_submittable_when_dn_edited(entity, manager, dn):
     assert not dn.is_ready_for_reading
     assert not dn.is_ready_for_reading
-    assert not manager._is_submittable(sequence)
+    assert not manager._is_submittable(entity)
     # Since it is a lazy property, the sequence is not yet in the dictionary
     # Since it is a lazy property, the sequence is not yet in the dictionary
-    assert sequence.id not in _ReadyToRunProperty._submittable_id_datanodes
+    assert entity.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
     dn.lock_edit()
     dn.lock_edit()
-    assert _ReadyToRunProperty._submittable_id_datanodes[sequence.id][dn.id] == {
+    assert _ReadyToRunProperty._submittable_id_datanodes[entity.id]._reasons[dn.id] == {
         f"DataNode {dn.id} is being edited",
         f"DataNode {dn.id} is being edited",
         f"DataNode {dn.id} is not written",
         f"DataNode {dn.id} is not written",
     }
     }
+    reason_str = _ReadyToRunProperty._submittable_id_datanodes[entity.id].reasons
+    assert f"DataNode {dn.id} is being edited" in reason_str
+    assert f"DataNode {dn.id} is not written" in reason_str
+
     dn.write("ANY VALUE")
     dn.write("ANY VALUE")
-    assert manager._is_submittable(sequence)
-    assert sequence.id not in _ReadyToRunProperty._submittable_id_datanodes
+    assert manager._is_submittable(entity)
+    assert isinstance(manager._is_submittable(entity), Reason)
+    assert entity.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert dn.id not in _ReadyToRunProperty._datanode_id_submittables
     assert dn.id not in _ReadyToRunProperty._datanode_id_submittables
 
 
 
 

+ 69 - 0
tests/core/common/test_reason.py

@@ -0,0 +1,69 @@
+# 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 taipy.core.common.reason import Reason
+
+
+def test_create_reason():
+    reason = Reason("entity_id")
+    assert reason.entity_id == "entity_id"
+    assert reason._reasons == {}
+    assert reason
+    assert not reason._entity_id_exists_in_reason("entity_id")
+    assert reason.reasons == ""
+
+
+def test_add_and_remove_reason():
+    reason = Reason("entity_id")
+    reason._add_reason("entity_id_1", "Some reason")
+    assert reason._reasons == {"entity_id_1": {"Some reason"}}
+    assert not reason
+    assert reason._entity_id_exists_in_reason("entity_id_1")
+    assert reason.reasons == "Some reason."
+
+    reason._add_reason("entity_id_1", "Another reason")
+    reason._add_reason("entity_id_2", "Some more reason")
+    assert reason._reasons == {"entity_id_1": {"Some reason", "Another reason"}, "entity_id_2": {"Some more reason"}}
+    assert not reason
+    assert reason._entity_id_exists_in_reason("entity_id_1")
+    assert reason._entity_id_exists_in_reason("entity_id_2")
+
+    reason._remove_reason("entity_id_1", "Some reason")
+    assert reason._reasons == {"entity_id_1": {"Another reason"}, "entity_id_2": {"Some more reason"}}
+    assert not reason
+    assert reason._entity_id_exists_in_reason("entity_id_1")
+    assert reason._entity_id_exists_in_reason("entity_id_2")
+
+    reason._remove_reason("entity_id_2", "Some more reason")
+    assert reason._reasons == {"entity_id_1": {"Another reason"}}
+    assert not reason
+    assert reason._entity_id_exists_in_reason("entity_id_1")
+    assert not reason._entity_id_exists_in_reason("entity_id_2")
+
+    reason._remove_reason("entity_id_1", "Another reason")
+    assert reason._reasons == {}
+    assert reason
+    assert not reason._entity_id_exists_in_reason("entity_id_1")
+
+
+def test_get_reason_string_from_reason():
+    reason = Reason("entity_id")
+    reason._add_reason("entity_id_1", "Some reason")
+    assert reason.reasons == "Some reason."
+
+    reason._add_reason("entity_id_2", "Some more reason")
+    assert reason.reasons == "Some reason; Some more reason."
+
+    reason._add_reason("entity_id_1", "Another reason")
+    assert reason.reasons.count(";") == 2
+    assert "Some reason" in reason.reasons
+    assert "Another reason" in reason.reasons
+    assert "Some more reason" in reason.reasons