Просмотр исходного кода

Merge pull request #1459 from Avaiga/feature/reasons-refacto

Reason class refactoring
Toan Quach 10 месяцев назад
Родитель
Сommit
b2cd473c4f

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

@@ -12,7 +12,7 @@
 from typing import TYPE_CHECKING, Dict, Set, Union
 from typing import TYPE_CHECKING, Dict, Set, Union
 
 
 from ..notification import EventOperation, Notifier, _make_event
 from ..notification import EventOperation, Notifier, _make_event
-from ..reason.reason import Reasons
+from ..reason import Reason, ReasonCollection
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from ..data.data_node import DataNode, DataNodeId
     from ..data.data_node import DataNode, DataNodeId
@@ -29,10 +29,10 @@ 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"], Reasons] = {}
+    _submittable_id_datanodes: Dict[Union["ScenarioId", "SequenceId", "TaskId"], ReasonCollection] = {}
 
 
     @classmethod
     @classmethod
-    def _add(cls, dn: "DataNode", reason: str) -> None:
+    def _add(cls, dn: "DataNode", reason: Reason) -> None:
         from ..scenario.scenario import Scenario
         from ..scenario.scenario import Scenario
         from ..sequence.sequence import Sequence
         from ..sequence.sequence import Sequence
         from ..task.task import Task
         from ..task.task import Task
@@ -50,7 +50,7 @@ class _ReadyToRunProperty:
                 cls.__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: Reason) -> None:
         from ..taipy import get as tp_get
         from ..taipy import get as tp_get
 
 
         # check the data node status to determine the reason to be removed
         # check the data node status to determine the reason to be removed
@@ -72,7 +72,7 @@ class _ReadyToRunProperty:
             cls._datanode_id_submittables.pop(datanode.id)
             cls._datanode_id_submittables.pop(datanode.id)
 
 
     @classmethod
     @classmethod
-    def __add(cls, submittable: Union["Scenario", "Sequence", "Task"], datanode: "DataNode", reason: str) -> None:
+    def __add(cls, submittable: Union["Scenario", "Sequence", "Task"], datanode: "DataNode", reason: Reason) -> None:
         if datanode.id not in cls._datanode_id_submittables:
         if datanode.id not in cls._datanode_id_submittables:
             cls._datanode_id_submittables[datanode.id] = set()
             cls._datanode_id_submittables[datanode.id] = set()
         cls._datanode_id_submittables[datanode.id].add(submittable.id)
         cls._datanode_id_submittables[datanode.id].add(submittable.id)
@@ -81,7 +81,7 @@ 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] = Reasons(submittable.id)
+            cls._submittable_id_datanodes[submittable.id] = ReasonCollection()
         cls._submittable_id_datanodes[submittable.id]._add_reason(datanode.id, reason)
         cls._submittable_id_datanodes[submittable.id]._add_reason(datanode.id, reason)
 
 
     @staticmethod
     @staticmethod

+ 6 - 7
taipy/core/_entity/submittable.py

@@ -19,8 +19,7 @@ from ..common._listattributes import _ListAttributes
 from ..common._utils import _Subscriber
 from ..common._utils import _Subscriber
 from ..data.data_node import DataNode
 from ..data.data_node import DataNode
 from ..job.job import Job
 from ..job.job import Job
-from ..reason._reason_factory import _build_data_node_is_being_edited_reason, _build_data_node_is_not_written
-from ..reason.reason import Reasons
+from ..reason import DataNodeEditInProgress, DataNodeIsNotWritten, ReasonCollection
 from ..submission.submission import Submission
 from ..submission.submission import Submission
 from ..task.task import Task
 from ..task.task import Task
 from ._dag import _DAG
 from ._dag import _DAG
@@ -83,22 +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) -> Reasons:
+    def is_ready_to_run(self) -> ReasonCollection:
         """Indicate if the entity is ready to be run.
         """Indicate if the entity is ready to be run.
 
 
         Returns:
         Returns:
             A Reason object that can function as a Boolean value.
             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.
             which is True if the given entity is ready to be run or there is no reason to be blocked, False otherwise.
         """
         """
-        reason = Reasons(self._submittable_id)
+        reason_collection = ReasonCollection()
 
 
         for node in self.get_inputs():
         for node in self.get_inputs():
             if node._edit_in_progress:
             if node._edit_in_progress:
-                reason._add_reason(node.id, _build_data_node_is_being_edited_reason(node.id))
+                reason_collection._add_reason(node.id, DataNodeEditInProgress(node.id))
             if not node._last_edit_date:
             if not node._last_edit_date:
-                reason._add_reason(node.id, _build_data_node_is_not_written(node.id))
+                reason_collection._add_reason(node.id, DataNodeIsNotWritten(node.id))
 
 
-        return reason
+        return reason_collection
 
 
     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.

+ 6 - 7
taipy/core/data/_data_manager.py

@@ -22,8 +22,7 @@ from ..config.data_node_config import DataNodeConfig
 from ..cycle.cycle_id import CycleId
 from ..cycle.cycle_id import CycleId
 from ..exceptions.exceptions import InvalidDataNodeType
 from ..exceptions.exceptions import InvalidDataNodeType
 from ..notification import Event, EventEntityType, EventOperation, Notifier, _make_event
 from ..notification import Event, EventEntityType, EventOperation, Notifier, _make_event
-from ..reason._reason_factory import _build_not_global_scope_reason, _build_wrong_config_type_reason
-from ..reason.reason import Reasons
+from ..reason import NotGlobalScope, ReasonCollection, WrongConfigType
 from ..scenario.scenario_id import ScenarioId
 from ..scenario.scenario_id import ScenarioId
 from ..sequence.sequence_id import SequenceId
 from ..sequence.sequence_id import SequenceId
 from ._data_fs_repository import _DataFSRepository
 from ._data_fs_repository import _DataFSRepository
@@ -69,17 +68,17 @@ class _DataManager(_Manager[DataNode], _VersionMixin):
         }
         }
 
 
     @classmethod
     @classmethod
-    def _can_create(cls, config: Optional[DataNodeConfig] = None) -> Reasons:
+    def _can_create(cls, config: Optional[DataNodeConfig] = None) -> ReasonCollection:
         config_id = getattr(config, "id", None) or str(config)
         config_id = getattr(config, "id", None) or str(config)
-        reason = Reasons(config_id)
+        reason_collection = ReasonCollection()
 
 
         if config is not None:
         if config is not None:
             if not isinstance(config, DataNodeConfig):
             if not isinstance(config, DataNodeConfig):
-                reason._add_reason(config_id, _build_wrong_config_type_reason(config_id, "DataNodeConfig"))
+                reason_collection._add_reason(config_id, WrongConfigType(config_id, DataNodeConfig.__name__))
             elif config.scope is not Scope.GLOBAL:
             elif config.scope is not Scope.GLOBAL:
-                reason._add_reason(config_id, _build_not_global_scope_reason(config_id))
+                reason_collection._add_reason(config_id, NotGlobalScope(config_id))
 
 
-        return reason
+        return reason_collection
 
 
     @classmethod
     @classmethod
     def _create_and_set(
     def _create_and_set(

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

@@ -32,6 +32,7 @@ from ..common._warnings import _warn_deprecated
 from ..exceptions.exceptions import DataNodeIsBeingEdited, NoData
 from ..exceptions.exceptions import DataNodeIsBeingEdited, NoData
 from ..job.job_id import JobId
 from ..job.job_id import JobId
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
+from ..reason import DataNodeEditInProgress, DataNodeIsNotWritten
 from ._filter import _FilterDataNode
 from ._filter import _FilterDataNode
 from .data_node_id import DataNodeId, Edit
 from .data_node_id import DataNodeId, Edit
 from .operator import JoinOperator
 from .operator import JoinOperator
@@ -43,13 +44,13 @@ def _update_ready_for_reading(fct):
     def _recompute_is_ready_for_reading(dn: "DataNode", *args, **kwargs):
     def _recompute_is_ready_for_reading(dn: "DataNode", *args, **kwargs):
         fct(dn, *args, **kwargs)
         fct(dn, *args, **kwargs)
         if dn._edit_in_progress:
         if dn._edit_in_progress:
-            _ReadyToRunProperty._add(dn, f"DataNode {dn.id} is being edited")
+            _ReadyToRunProperty._add(dn, DataNodeEditInProgress(dn.id))
         else:
         else:
-            _ReadyToRunProperty._remove(dn, f"DataNode {dn.id} is being edited")
+            _ReadyToRunProperty._remove(dn, DataNodeEditInProgress(dn.id))
         if not dn._last_edit_date:
         if not dn._last_edit_date:
-            _ReadyToRunProperty._add(dn, f"DataNode {dn.id} is not written")
+            _ReadyToRunProperty._add(dn, DataNodeIsNotWritten(dn.id))
         else:
         else:
-            _ReadyToRunProperty._remove(dn, f"DataNode {dn.id} is not written")
+            _ReadyToRunProperty._remove(dn, DataNodeIsNotWritten(dn.id))
 
 
     return _recompute_is_ready_for_reading
     return _recompute_is_ready_for_reading
 
 
@@ -396,7 +397,7 @@ class DataNode(_Entity, _Labeled):
             return self.read_or_raise()
             return self.read_or_raise()
         except NoData:
         except NoData:
             self.__logger.warning(
             self.__logger.warning(
-                f"Data node {self.id} from config {self.config_id} is being read but has never been " f"written."
+                f"Data node {self.id} from config {self.config_id} is being read but has never been written."
             )
             )
             return None
             return None
 
 

+ 9 - 1
taipy/core/reason/__init__.py

@@ -9,4 +9,12 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # 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.
 # specific language governing permissions and limitations under the License.
 
 
-from .reason import Reasons
+from .reason import (
+    DataNodeEditInProgress,
+    DataNodeIsNotWritten,
+    EntityIsNotSubmittableEntity,
+    NotGlobalScope,
+    Reason,
+    WrongConfigType,
+)
+from .reason_collection import ReasonCollection

+ 0 - 37
taipy/core/reason/_reason_factory.py

@@ -1,37 +0,0 @@
-# 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 Optional
-
-from ..data.data_node import DataNodeId
-
-
-def _build_data_node_is_being_edited_reason(dn_id: DataNodeId) -> str:
-    return f"DataNode {dn_id} is being edited"
-
-
-def _build_data_node_is_not_written(dn_id: DataNodeId) -> str:
-    return f"DataNode {dn_id} is not written"
-
-
-def _build_not_submittable_entity_reason(entity_id: str) -> str:
-    return f"Entity {entity_id} is not a submittable entity"
-
-
-def _build_wrong_config_type_reason(config_id: str, config_type: Optional[str]) -> str:
-    if config_type:
-        return f'Object "{config_id}" must be a valid {config_type}'
-
-    return f'Object "{config_id}" is not a valid config to be created'
-
-
-def _build_not_global_scope_reason(config_id: str) -> str:
-    return f'Data node config "{config_id}" does not have GLOBAL scope'

+ 106 - 22
taipy/core/reason/reason.py

@@ -9,33 +9,117 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # 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.
 # specific language governing permissions and limitations under the License.
 
 
-from typing import Dict, Set
+from typing import Any, Optional
 
 
 
 
-class Reasons:
-    def __init__(self, entity_id: str) -> None:
-        self.entity_id: str = entity_id
-        self._reasons: Dict[str, Set[str]] = {}
+class Reason:
+    """
+    A reason explains why a specific action cannot be performed.
 
 
-    def _add_reason(self, entity_id: str, reason: str) -> "Reasons":
-        if entity_id not in self._reasons:
-            self._reasons[entity_id] = set()
-        self._reasons[entity_id].add(reason)
-        return self
+    This is a parent class aiming at being implemented by specific sub-classes.
 
 
-    def _remove_reason(self, entity_id: str, reason: str) -> "Reasons":
-        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
+    Because Taipy applications are natively multiuser, asynchronous, and dynamic,
+    some functions might not be called in some specific contexts. You can protect
+    such calls by calling other methods that return a Reasons object. It acts like a
+    boolean: True if the operation can be performed and False otherwise.
+    If the action cannot be performed, the Reasons object holds all the `reasons as a list
+    of `Reason` objects. Each `Reason` holds an explanation of why the operation cannot be
+    performed.
 
 
-    def _entity_id_exists_in_reason(self, entity_id: str) -> bool:
-        return entity_id in self._reasons
+    Attributes:
+        reason (str): The English representation of the reason why the action cannot be performed.
+    """
 
 
-    def __bool__(self) -> bool:
-        return len(self._reasons) == 0
+    def __init__(self, reason: str):
+        self._reason = reason
+
+    def __str__(self) -> str:
+        return self._reason
+
+    def __repr__(self) -> str:
+        return self._reason
+
+    def __hash__(self) -> int:
+        return hash(self._reason)
+
+    def __eq__(self, value: Any) -> bool:
+        return isinstance(value, Reason) and value._reason == self._reason
+
+
+class _DataNodeReasonMixin:
+    def __init__(self, datanode_id: str):
+        self.datanode_id = datanode_id
 
 
     @property
     @property
-    def reasons(self) -> str:
-        return "; ".join("; ".join(reason) for reason in self._reasons.values()) + "." if self._reasons else ""
+    def datanode(self):
+        from ..data._data_manager_factory import _DataManagerFactory
+
+        return _DataManagerFactory._build_manager()._get(self.datanode_id)
+
+
+class DataNodeEditInProgress(Reason, _DataNodeReasonMixin):
+    """
+    A `DataNode^` is being edited, which prevents specific actions from being performed.
+
+    Attributes:
+        datanode_id (str): The identifier of the `DataNode^`.
+    """
+
+    def __init__(self, datanode_id: str):
+        Reason.__init__(self, f"DataNode {datanode_id} is being edited")
+        _DataNodeReasonMixin.__init__(self, datanode_id)
+
+
+class DataNodeIsNotWritten(Reason, _DataNodeReasonMixin):
+    """
+    A `DataNode^` has never been written, which prevents specific actions from being performed.
+
+    Attributes:
+        datanode_id (str): The identifier of the `DataNode^`.
+    """
+
+    def __init__(self, datanode_id: str):
+        Reason.__init__(self, f"DataNode {datanode_id} is not written")
+        _DataNodeReasonMixin.__init__(self, datanode_id)
+
+
+class EntityIsNotSubmittableEntity(Reason):
+    """
+    An entity is not a submittable entity, which prevents specific actions from being performed.
+
+    Attributes:
+        entity_id (str): The identifier of the `Entity^`.
+    """
+
+    def __init__(self, entity_id: str):
+        Reason.__init__(self, f"Entity {entity_id} is not a submittable entity")
+
+
+class WrongConfigType(Reason):
+    """
+    A config id is not a valid expected config, which prevents specific actions from being performed.
+
+    Attributes:
+        config_id (str): The identifier of the config.
+        config_type (str): The expected config type.
+    """
+
+    def __init__(self, config_id: str, config_type: Optional[str]):
+        if config_type:
+            reason = f'Object "{config_id}" must be a valid {config_type}'
+        else:
+            reason = f'Object "{config_id}" is not a valid config to be created'
+
+        Reason.__init__(self, reason)
+
+
+class NotGlobalScope(Reason):
+    """
+    A data node config does not have a GLOBAL scope, which prevents specific actions from being performed.
+
+    Attributes:
+        config_id (str): The identifier of the config.
+    """
+
+    def __init__(self, config_id: str):
+        Reason.__init__(self, f'Data node config "{config_id}" does not have GLOBAL scope')

+ 60 - 0
taipy/core/reason/reason_collection.py

@@ -0,0 +1,60 @@
+# 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
+
+from .reason import Reason
+
+
+class ReasonCollection:
+    """
+    This class is used to store all the reasons to explain why some Taipy operations are not allowed.
+
+    Because Taipy applications are natively multiuser, asynchronous, and dynamic,
+    some functions might not be called in some specific contexts. You can protect
+    such calls by calling other methods that return a `ReasonCollection`. It acts like a
+    boolean: True if the operation can be performed and False otherwise.
+    If the action cannot be performed, the ReasonCollection holds all the individual reasons as a list
+    of `Reason` objects. Each `Reason` explains why the operation cannot be performed.
+    """
+
+    def __init__(self) -> None:
+        self._reasons: Dict[str, Set[Reason]] = {}
+
+    def _add_reason(self, entity_id: str, reason: Reason) -> "ReasonCollection":
+        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: Reason) -> "ReasonCollection":
+        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:
+        """Retrieves a collection of reasons as a string that explains why the action cannot be performed.
+
+        Returns:
+            A string that contains all the reasons why the action cannot be performed.
+        """
+        if self._reasons:
+            return "; ".join("; ".join([str(reason) for reason in reasons]) for reasons in self._reasons.values()) + "."
+        return ""

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

@@ -39,8 +39,7 @@ from ..exceptions.exceptions import (
 from ..job._job_manager_factory import _JobManagerFactory
 from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..job.job import Job
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
-from ..reason._reason_factory import _build_not_submittable_entity_reason, _build_wrong_config_type_reason
-from ..reason.reason import Reasons
+from ..reason import EntityIsNotSubmittableEntity, ReasonCollection, WrongConfigType
 from ..submission._submission_manager_factory import _SubmissionManagerFactory
 from ..submission._submission_manager_factory import _SubmissionManagerFactory
 from ..submission.submission import Submission
 from ..submission.submission import Submission
 from ..task._task_manager_factory import _TaskManagerFactory
 from ..task._task_manager_factory import _TaskManagerFactory
@@ -108,15 +107,15 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         )
         )
 
 
     @classmethod
     @classmethod
-    def _can_create(cls, config: Optional[ScenarioConfig] = None) -> Reasons:
+    def _can_create(cls, config: Optional[ScenarioConfig] = None) -> ReasonCollection:
         config_id = getattr(config, "id", None) or str(config)
         config_id = getattr(config, "id", None) or str(config)
-        reason = Reasons(config_id)
+        reason_collector = ReasonCollection()
 
 
         if config is not None:
         if config is not None:
             if not isinstance(config, ScenarioConfig):
             if not isinstance(config, ScenarioConfig):
-                reason._add_reason(config_id, _build_wrong_config_type_reason(config_id, "ScenarioConfig"))
+                reason_collector._add_reason(config_id, WrongConfigType(config_id, ScenarioConfig.__name__))
 
 
-        return reason
+        return reason_collector
 
 
     @classmethod
     @classmethod
     def _create(
     def _create(
@@ -202,15 +201,15 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return scenario
         return scenario
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> Reasons:
+    def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> ReasonCollection:
         if isinstance(scenario, str):
         if isinstance(scenario, str):
             scenario = cls._get(scenario)
             scenario = cls._get(scenario)
 
 
         if not isinstance(scenario, Scenario):
         if not isinstance(scenario, Scenario):
             scenario = str(scenario)
             scenario = str(scenario)
-            reason = Reasons((scenario))
-            reason._add_reason(scenario, _build_not_submittable_entity_reason(scenario))
-            return reason
+            reason_collector = ReasonCollection()
+            reason_collector._add_reason(scenario, EntityIsNotSubmittableEntity(scenario))
+            return reason_collector
 
 
         return scenario.is_ready_to_run()
         return scenario.is_ready_to_run()
 
 

+ 5 - 6
taipy/core/sequence/_sequence_manager.py

@@ -29,8 +29,7 @@ from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..job.job import Job
 from ..notification import Event, EventEntityType, EventOperation, Notifier
 from ..notification import Event, EventEntityType, EventOperation, Notifier
 from ..notification.event import _make_event
 from ..notification.event import _make_event
-from ..reason._reason_factory import _build_not_submittable_entity_reason
-from ..reason.reason import Reasons
+from ..reason import EntityIsNotSubmittableEntity, ReasonCollection
 from ..scenario._scenario_manager_factory import _ScenarioManagerFactory
 from ..scenario._scenario_manager_factory import _ScenarioManagerFactory
 from ..scenario.scenario import Scenario
 from ..scenario.scenario import Scenario
 from ..scenario.scenario_id import ScenarioId
 from ..scenario.scenario_id import ScenarioId
@@ -342,15 +341,15 @@ 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]) -> Reasons:
+    def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> ReasonCollection:
         if isinstance(sequence, str):
         if isinstance(sequence, str):
             sequence = cls._get(sequence)
             sequence = cls._get(sequence)
 
 
         if not isinstance(sequence, Sequence):
         if not isinstance(sequence, Sequence):
             sequence = str(sequence)
             sequence = str(sequence)
-            reason = Reasons(sequence)
-            reason._add_reason(sequence, _build_not_submittable_entity_reason(sequence))
-            return reason
+            reason_collector = ReasonCollection()
+            reason_collector._add_reason(sequence, EntityIsNotSubmittableEntity(sequence))
+            return reason_collector
 
 
         return sequence.is_ready_to_run()
         return sequence.is_ready_to_run()
 
 

+ 4 - 5
taipy/core/taipy.py

@@ -45,8 +45,7 @@ from .exceptions.exceptions import (
 from .job._job_manager_factory import _JobManagerFactory
 from .job._job_manager_factory import _JobManagerFactory
 from .job.job import Job
 from .job.job import Job
 from .job.job_id import JobId
 from .job.job_id import JobId
-from .reason._reason_factory import _build_not_submittable_entity_reason
-from .reason.reason import Reasons
+from .reason import EntityIsNotSubmittableEntity, ReasonCollection
 from .scenario._scenario_manager_factory import _ScenarioManagerFactory
 from .scenario._scenario_manager_factory import _ScenarioManagerFactory
 from .scenario.scenario import Scenario
 from .scenario.scenario import Scenario
 from .scenario.scenario_id import ScenarioId
 from .scenario.scenario_id import ScenarioId
@@ -85,7 +84,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]) -> Reasons:
+def is_submittable(entity: Union[Scenario, ScenarioId, Sequence, SequenceId, Task, TaskId, str]) -> ReasonCollection:
     """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.
@@ -105,7 +104,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 Reasons(str(entity))._add_reason(str(entity), _build_not_submittable_entity_reason(str(entity)))
+    return ReasonCollection()._add_reason(str(entity), EntityIsNotSubmittableEntity(str(entity)))
 
 
 
 
 def is_editable(
 def is_editable(
@@ -880,7 +879,7 @@ def get_cycles() -> List[Cycle]:
     return _CycleManagerFactory._build_manager()._get_all()
     return _CycleManagerFactory._build_manager()._get_all()
 
 
 
 
-def can_create(config: Optional[Union[ScenarioConfig, DataNodeConfig]] = None) -> Reasons:
+def can_create(config: Optional[Union[ScenarioConfig, DataNodeConfig]] = None) -> ReasonCollection:
     """Indicate if a config can be created. The config should be a scenario or data node config.
     """Indicate if a config can be created. The config should be a scenario or data node config.
 
 
     If no config is provided, the function indicates if any scenario or data node config can be created.
     If no config is provided, the function indicates if any scenario or data node config can be created.

+ 8 - 13
taipy/core/task/_task_manager.py

@@ -26,12 +26,7 @@ from ..cycle.cycle_id import CycleId
 from ..data._data_manager_factory import _DataManagerFactory
 from ..data._data_manager_factory import _DataManagerFactory
 from ..exceptions.exceptions import NonExistingTask
 from ..exceptions.exceptions import NonExistingTask
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
-from ..reason._reason_factory import (
-    _build_data_node_is_being_edited_reason,
-    _build_data_node_is_not_written,
-    _build_not_submittable_entity_reason,
-)
-from ..reason.reason import Reasons
+from ..reason import DataNodeEditInProgress, DataNodeIsNotWritten, EntityIsNotSubmittableEntity, ReasonCollection
 from ..scenario.scenario_id import ScenarioId
 from ..scenario.scenario_id import ScenarioId
 from ..sequence.sequence_id import SequenceId
 from ..sequence.sequence_id import SequenceId
 from ..submission.submission import Submission
 from ..submission.submission import Submission
@@ -169,24 +164,24 @@ class _TaskManager(_Manager[Task], _VersionMixin):
         return entity_ids
         return entity_ids
 
 
     @classmethod
     @classmethod
-    def _is_submittable(cls, task: Union[Task, TaskId]) -> Reasons:
+    def _is_submittable(cls, task: Union[Task, TaskId]) -> ReasonCollection:
         if isinstance(task, str):
         if isinstance(task, str):
             task = cls._get(task)
             task = cls._get(task)
+
+        reason_collection = ReasonCollection()
         if not isinstance(task, Task):
         if not isinstance(task, Task):
             task = str(task)
             task = str(task)
-            reason = Reasons(task)
-            reason._add_reason(task, _build_not_submittable_entity_reason(task))
+            reason_collection._add_reason(task, EntityIsNotSubmittableEntity(task))
         else:
         else:
-            reason = Reasons(task.id)
             data_manager = _DataManagerFactory._build_manager()
             data_manager = _DataManagerFactory._build_manager()
             for node in task.input.values():
             for node in task.input.values():
                 node = data_manager._get(node)
                 node = data_manager._get(node)
                 if node._edit_in_progress:
                 if node._edit_in_progress:
-                    reason._add_reason(node.id, _build_data_node_is_being_edited_reason(node.id))
+                    reason_collection._add_reason(node.id, DataNodeEditInProgress(node.id))
                 if not node._last_edit_date:
                 if not node._last_edit_date:
-                    reason._add_reason(node.id, _build_data_node_is_not_written(node.id))
+                    reason_collection._add_reason(node.id, DataNodeIsNotWritten(node.id))
 
 
-        return reason
+        return reason_collection
 
 
     @classmethod
     @classmethod
     def _submit(
     def _submit(

+ 18 - 18
tests/core/_entity/test_ready_to_run_property.py

@@ -14,7 +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.reason.reason import Reasons
+from taipy.core.reason import DataNodeEditInProgress, DataNodeIsNotWritten, ReasonCollection
 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
@@ -33,7 +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), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
 
 
@@ -46,7 +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), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
 
 
@@ -61,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 isinstance(scenario_manager._is_submittable(scenario), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
     assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes
 
 
 
 
@@ -78,7 +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), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
 
 
     # 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
@@ -97,14 +97,14 @@ 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), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
 
 
     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]._reasons
     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]._reasons[dn_1.id] == {
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
-        f"DataNode {dn_1.id} is being edited"
+        DataNodeEditInProgress(dn_1.id)
     }
     }
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons == f"DataNode {dn_1.id} is being edited."
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons == f"DataNode {dn_1.id} is being edited."
 
 
@@ -125,7 +125,7 @@ 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), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
 
 
     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]._reasons
     assert dn_1.id in _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons
@@ -134,12 +134,12 @@ def test_scenario_not_submittable_for_multiple_reasons():
     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]._reasons[dn_1.id] == {
     assert _ReadyToRunProperty._submittable_id_datanodes[scenario.id]._reasons[dn_1.id] == {
-        f"DataNode {dn_1.id} is being edited"
+        DataNodeEditInProgress(dn_1.id)
     }
     }
     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]._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",
+        DataNodeEditInProgress(dn_2.id),
+        DataNodeIsNotWritten(dn_2.id),
     }
     }
     reason_str = _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons
     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 being edited" in reason_str
@@ -156,14 +156,14 @@ 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), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     # 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]._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",
+        DataNodeEditInProgress(dn_1.id),
+        DataNodeIsNotWritten(dn_1.id),
     }
     }
     reason_str = _ReadyToRunProperty._submittable_id_datanodes[scenario.id].reasons
     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 being edited" in reason_str
@@ -171,7 +171,7 @@ def test_writing_input_remove_reasons():
 
 
     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), Reasons)
+    assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection)
     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
 
 
@@ -188,8 +188,8 @@ def __assert_not_submittable_becomes_submittable_when_dn_edited(entity, manager,
 
 
     dn.lock_edit()
     dn.lock_edit()
     assert _ReadyToRunProperty._submittable_id_datanodes[entity.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",
+        DataNodeEditInProgress(dn.id),
+        DataNodeIsNotWritten(dn.id),
     }
     }
     reason_str = _ReadyToRunProperty._submittable_id_datanodes[entity.id].reasons
     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 being edited" in reason_str
@@ -197,7 +197,7 @@ def __assert_not_submittable_becomes_submittable_when_dn_edited(entity, manager,
 
 
     dn.write("ANY VALUE")
     dn.write("ANY VALUE")
     assert manager._is_submittable(entity)
     assert manager._is_submittable(entity)
-    assert isinstance(manager._is_submittable(entity), Reasons)
+    assert isinstance(manager._is_submittable(entity), ReasonCollection)
     assert entity.id not in _ReadyToRunProperty._submittable_id_datanodes
     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
 
 

+ 45 - 43
tests/core/common/test_reason.py

@@ -9,61 +9,63 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # 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.
 # specific language governing permissions and limitations under the License.
 
 
-from taipy.core.reason.reason import Reasons
+from taipy.core.reason import ReasonCollection
 
 
 
 
 def test_create_reason():
 def test_create_reason():
-    reason = Reasons("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 == ""
+    reason_collection = ReasonCollection()
+    assert reason_collection._reasons == {}
+    assert reason_collection
+    assert not reason_collection._entity_id_exists_in_reason("entity_id")
+    assert reason_collection.reasons == ""
 
 
 
 
 def test_add_and_remove_reason():
 def test_add_and_remove_reason():
-    reason = Reasons("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_collection = ReasonCollection()
+    reason_collection._add_reason("entity_id_1", "Some reason")
+    assert reason_collection._reasons == {"entity_id_1": {"Some reason"}}
+    assert not reason_collection
+    assert reason_collection._entity_id_exists_in_reason("entity_id_1")
+    assert reason_collection.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_collection._add_reason("entity_id_1", "Another reason")
+    reason_collection._add_reason("entity_id_2", "Some more reason")
+    assert reason_collection._reasons == {
+        "entity_id_1": {"Some reason", "Another reason"},
+        "entity_id_2": {"Some more reason"},
+    }
+    assert not reason_collection
+    assert reason_collection._entity_id_exists_in_reason("entity_id_1")
+    assert reason_collection._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_collection._remove_reason("entity_id_1", "Some reason")
+    assert reason_collection._reasons == {"entity_id_1": {"Another reason"}, "entity_id_2": {"Some more reason"}}
+    assert not reason_collection
+    assert reason_collection._entity_id_exists_in_reason("entity_id_1")
+    assert reason_collection._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_collection._remove_reason("entity_id_2", "Some more reason")
+    assert reason_collection._reasons == {"entity_id_1": {"Another reason"}}
+    assert not reason_collection
+    assert reason_collection._entity_id_exists_in_reason("entity_id_1")
+    assert not reason_collection._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")
+    reason_collection._remove_reason("entity_id_1", "Another reason")
+    assert reason_collection._reasons == {}
+    assert reason_collection
+    assert not reason_collection._entity_id_exists_in_reason("entity_id_1")
 
 
 
 
 def test_get_reason_string_from_reason():
 def test_get_reason_string_from_reason():
-    reason = Reasons("entity_id")
-    reason._add_reason("entity_id_1", "Some reason")
-    assert reason.reasons == "Some reason."
+    reason_collection = ReasonCollection()
+    reason_collection._add_reason("entity_id_1", "Some reason")
+    assert reason_collection.reasons == "Some reason."
 
 
-    reason._add_reason("entity_id_2", "Some more reason")
-    assert reason.reasons == "Some reason; Some more reason."
+    reason_collection._add_reason("entity_id_2", "Some more reason")
+    assert reason_collection.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
+    reason_collection._add_reason("entity_id_1", "Another reason")
+    assert reason_collection.reasons.count(";") == 2
+    assert "Some reason" in reason_collection.reasons
+    assert "Another reason" in reason_collection.reasons
+    assert "Some more reason" in reason_collection.reasons

+ 8 - 2
tests/core/data/test_data_manager.py

@@ -24,6 +24,7 @@ from taipy.core.data.data_node_id import DataNodeId
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.exceptions.exceptions import InvalidDataNodeType, ModelNotFound
 from taipy.core.exceptions.exceptions import InvalidDataNodeType, ModelNotFound
+from taipy.core.reason import NotGlobalScope, WrongConfigType
 from tests.core.utils.named_temporary_file import NamedTemporaryFile
 from tests.core.utils.named_temporary_file import NamedTemporaryFile
 
 
 
 
@@ -61,11 +62,16 @@ class TestDataManager:
 
 
         reasons = _DataManager._can_create(dn_config)
         reasons = _DataManager._can_create(dn_config)
         assert bool(reasons) is False
         assert bool(reasons) is False
-        assert reasons._reasons == {dn_config.id: {'Data node config "dn" does not have GLOBAL scope'}}
+        assert reasons._reasons[dn_config.id] == {NotGlobalScope(dn_config.id)}
+        assert (
+            str(list(reasons._reasons[dn_config.id])[0])
+            == f'Data node config "{dn_config.id}" does not have GLOBAL scope'
+        )
 
 
         reasons = _DataManager._can_create(1)
         reasons = _DataManager._can_create(1)
         assert bool(reasons) is False
         assert bool(reasons) is False
-        assert reasons._reasons == {"1": {'Object "1" must be a valid DataNodeConfig'}}
+        assert reasons._reasons["1"] == {WrongConfigType("1", DataNodeConfig.__name__)}
+        assert str(list(reasons._reasons["1"])[0]) == 'Object "1" must be a valid DataNodeConfig'
 
 
     def test_create_data_node_with_name_provided(self):
     def test_create_data_node_with_name_provided(self):
         dn_config = Config.configure_data_node(id="dn", foo="bar", name="acb")
         dn_config = Config.configure_data_node(id="dn", foo="bar", name="acb")

+ 6 - 2
tests/core/scenario/test_scenario_manager.py

@@ -25,6 +25,7 @@ from taipy.core._orchestrator._orchestrator import _Orchestrator
 from taipy.core._version._version_manager import _VersionManager
 from taipy.core._version._version_manager import _VersionManager
 from taipy.core.common import _utils
 from taipy.core.common import _utils
 from taipy.core.common._utils import _Subscriber
 from taipy.core.common._utils import _Subscriber
+from taipy.core.config.scenario_config import ScenarioConfig
 from taipy.core.cycle._cycle_manager import _CycleManager
 from taipy.core.cycle._cycle_manager import _CycleManager
 from taipy.core.data._data_manager import _DataManager
 from taipy.core.data._data_manager import _DataManager
 from taipy.core.data.in_memory import InMemoryDataNode
 from taipy.core.data.in_memory import InMemoryDataNode
@@ -40,6 +41,7 @@ from taipy.core.exceptions.exceptions import (
     UnauthorizedTagError,
     UnauthorizedTagError,
 )
 )
 from taipy.core.job._job_manager import _JobManager
 from taipy.core.job._job_manager import _JobManager
+from taipy.core.reason import WrongConfigType
 from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario.scenario import Scenario
 from taipy.core.scenario.scenario import Scenario
@@ -382,13 +384,15 @@ def test_can_create():
 
 
     reasons = _ScenarioManager._can_create(task_config)
     reasons = _ScenarioManager._can_create(task_config)
     assert bool(reasons) is False
     assert bool(reasons) is False
-    assert reasons._reasons == {task_config.id: {'Object "task" must be a valid ScenarioConfig'}}
+    assert reasons._reasons[task_config.id] == {WrongConfigType(task_config.id, ScenarioConfig.__name__)}
+    assert str(list(reasons._reasons[task_config.id])[0]) == 'Object "task" must be a valid ScenarioConfig'
     with pytest.raises(AttributeError):
     with pytest.raises(AttributeError):
         _ScenarioManager._create(task_config)
         _ScenarioManager._create(task_config)
 
 
     reasons = _ScenarioManager._can_create(1)
     reasons = _ScenarioManager._can_create(1)
     assert bool(reasons) is False
     assert bool(reasons) is False
-    assert reasons._reasons == {"1": {'Object "1" must be a valid ScenarioConfig'}}
+    assert reasons._reasons["1"] == {WrongConfigType(1, ScenarioConfig.__name__)}
+    assert str(list(reasons._reasons["1"])[0]) == 'Object "1" must be a valid ScenarioConfig'
     with pytest.raises(AttributeError):
     with pytest.raises(AttributeError):
         _ScenarioManager._create(1)
         _ScenarioManager._create(1)
 
 

+ 6 - 6
tests/gui_core/test_context_is_submitable.py

@@ -14,7 +14,7 @@ from unittest.mock import Mock, patch
 from taipy.config.common.scope import Scope
 from taipy.config.common.scope import Scope
 from taipy.core import Job, JobId, Scenario, Task
 from taipy.core import Job, JobId, Scenario, Task
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.data.pickle import PickleDataNode
-from taipy.core.reason.reason import Reasons
+from taipy.core.reason import ReasonCollection
 from taipy.gui_core._context import _GuiCoreContext
 from taipy.gui_core._context import _GuiCoreContext
 
 
 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}})
 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}})
@@ -25,13 +25,13 @@ a_datanode = PickleDataNode("data_node_config_id", Scope.SCENARIO)
 
 
 
 
 def mock_is_submittable_reason(entity_id):
 def mock_is_submittable_reason(entity_id):
-    reason = Reasons(entity_id)
-    reason._add_reason(entity_id, "a reason")
-    return reason
+    reasons = ReasonCollection()
+    reasons._add_reason(entity_id, "a reason")
+    return reasons
 
 
 
 
-def mock_has_no_reason(entity_id):
-    return Reasons(entity_id)
+def mock_has_no_reason():
+    return ReasonCollection()
 
 
 
 
 def mock_core_get(entity_id):
 def mock_core_get(entity_id):