浏览代码

added Reason return type to is_submittable functions, added and fixed test

Toan Quach 1 年之前
父节点
当前提交
352e5866d9

+ 1 - 1
taipy/core/_entity/_ready_to_run_property.py

@@ -11,7 +11,7 @@
 
 from typing import TYPE_CHECKING, Dict, Set, Union
 
-from ..common._reason import Reason
+from ..common.reason import Reason
 from ..notification import EventOperation, Notifier, _make_event
 
 if TYPE_CHECKING:

+ 12 - 2
taipy/core/_entity/submittable.py

@@ -17,6 +17,7 @@ import networkx as nx
 
 from ..common._listattributes import _ListAttributes
 from ..common._utils import _Subscriber
+from ..common.reason import Reason
 from ..data.data_node import DataNode
 from ..job.job import Job
 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)}
         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.
 
         Returns:
+            A Reason object that can function as a Boolean value.
             True if the given entity is ready to be run. 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]:
         """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()
     _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
     def _delete_all(cls):
         """

+ 18 - 12
taipy/core/common/_reason.py → taipy/core/common/reason.py

@@ -15,21 +15,27 @@ 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]] = {}
+        self._reasons: Dict[str, Set[str]] = {}
 
-    def _add_reason(self, entity_id: str, reason: str) -> None:
-        if entity_id not in self.reasons:
-            self.reasons[entity_id] = set()
-        self.reasons[entity_id].add(reason)
+    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) -> None:
-        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]
+    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
+        return entity_id in self._reasons
 
     def __bool__(self) -> bool:
-        return len(self.reasons) == 0
+        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

@@ -196,6 +196,9 @@ class DataNode(_Entity, _Labeled):
     def last_edit_date(self, val):
         self._last_edit_date = val
 
+    def _build_not_written_reason(self) -> str:
+        return f"DataNode {self.id} is not written"
+
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     def scope(self):
@@ -261,6 +264,9 @@ class DataNode(_Entity, _Labeled):
     def edit_in_progress(self, 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
     @_self_reload(_MANAGER_NAME)
     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 .._version._version_manager_factory import _VersionManagerFactory
 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 ..config.scenario_config import ScenarioConfig
 from ..cycle._cycle_manager_factory import _CycleManagerFactory
@@ -199,10 +200,17 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return scenario
 
     @classmethod
-    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> bool:
+    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> Reason:
         if isinstance(scenario, str):
             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
     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 .._version._version_mixin import _VersionMixin
 from ..common._utils import _Subscriber
+from ..common.reason import Reason
 from ..common.warn_if_inputs_not_ready import _warn_if_inputs_not_ready
 from ..exceptions.exceptions import (
     InvalidSequenceId,
@@ -296,10 +297,17 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         Notifier.publish(_make_event(sequence, EventOperation.UPDATE, attribute_name="subscribers"))
 
     @classmethod
-    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> bool:
+    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> Reason:
         if isinstance(sequence, str):
             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
     def _submit(

+ 3 - 2
taipy/core/taipy.py

@@ -33,6 +33,7 @@ from .common._check_instance import (
     _is_task,
 )
 from .common._warnings import _warn_deprecated, _warn_no_core_service
+from .common.reason import Reason
 from .config.data_node_config import DataNodeConfig
 from .config.scenario_config import ScenarioConfig
 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)
 
 
-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.
 
     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)
     if isinstance(entity, str) and entity.startswith(Task._ID_PREFIX):
         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(

+ 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 .._version._version_manager_factory import _VersionManagerFactory
 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 ..config.task_config import TaskConfig
 from ..cycle.cycle_id import CycleId
@@ -163,10 +164,24 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return entity_ids
 
     @classmethod
-    def _is_submittable(cls, task: Union[Task, TaskId]) -> bool:
+    def _is_submittable(cls, task: Union[Task, TaskId]) -> Reason:
         if isinstance(task, str):
             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
     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.config import Config
 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.sequence._sequence_manager_factory import _SequenceManagerFactory
 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)
 
     assert scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
     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)
 
     assert scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
     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
 
     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
 
 
@@ -75,6 +78,7 @@ def test_scenario_not_submittable_not_in_property_because_it_is_lazy():
     assert dn_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     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
     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 scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
 
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
-    assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons
+    assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
     assert dn_1.id in _ReadyToRunProperty._datanode_id_submittables
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
-    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons[dn_1.id] == {
+    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
         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():
@@ -119,21 +125,25 @@ def test_scenario_not_submittable_for_multiple_reasons():
     assert not dn_1.is_ready_for_reading
     assert not dn_2.is_ready_for_reading
     assert not scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
 
     assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes
-    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._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_2.id in _ReadyToRunProperty._datanode_id_submittables
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_1.id]
-    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons[dn_1.id] == {
+    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
         f"DataNode {dn_1.id} is being edited"
     }
     assert scenario.id in _ReadyToRunProperty._datanode_id_submittables[dn_2.id]
-    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons[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 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():
@@ -146,17 +156,22 @@ def test_writing_input_remove_reasons():
 
     assert not dn_1.is_ready_for_reading
     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
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
     dn_1.lock_edit()
-    assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons[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 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)
     assert scenario_manager._is_submittable(scenario)
+    assert isinstance(scenario_manager._is_submittable(scenario), Reason)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert dn_1.id not in _ReadyToRunProperty._datanode_id_submittables
 
@@ -165,20 +180,25 @@ def identity(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 manager._is_submittable(sequence)
+    assert not manager._is_submittable(entity)
     # 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()
-    assert _ReadyToRunProperty._submittable_id_datanodes[sequence.id].reasons[dn.id] == {
+    assert _ReadyToRunProperty._submittable_id_datanodes[entity.id]._reasons[dn.id] == {
         f"DataNode {dn.id} is being edited",
         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")
-    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
 
 

+ 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