diff --git a/taipy/core/_entity/_ready_to_run_property.py b/taipy/core/_entity/_ready_to_run_property.py index 6ad087b0df..c0e92e94ab 100644 --- a/taipy/core/_entity/_ready_to_run_property.py +++ b/taipy/core/_entity/_ready_to_run_property.py @@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Dict, Set, Union from ..notification import EventOperation, Notifier, _make_event -from ..reason.reason import Reasons +from ..reason import Reason, ReasonCollection if TYPE_CHECKING: from ..data.data_node import DataNode, DataNodeId @@ -29,10 +29,10 @@ class _ReadyToRunProperty: # 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) - _submittable_id_datanodes: Dict[Union["ScenarioId", "SequenceId", "TaskId"], Reasons] = {} + _submittable_id_datanodes: Dict[Union["ScenarioId", "SequenceId", "TaskId"], ReasonCollection] = {} @classmethod - def _add(cls, dn: "DataNode", reason: str) -> None: + def _add(cls, dn: "DataNode", reason: Reason) -> None: from ..scenario.scenario import Scenario from ..sequence.sequence import Sequence from ..task.task import Task @@ -50,7 +50,7 @@ def _add(cls, dn: "DataNode", reason: str) -> None: cls.__add(task_parent, dn, reason) @classmethod - def _remove(cls, datanode: "DataNode", reason: str) -> None: + def _remove(cls, datanode: "DataNode", reason: Reason) -> None: from ..taipy import get as tp_get # check the data node status to determine the reason to be removed @@ -72,7 +72,7 @@ def _remove(cls, datanode: "DataNode", reason: str) -> None: cls._datanode_id_submittables.pop(datanode.id) @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: cls._datanode_id_submittables[datanode.id] = set() cls._datanode_id_submittables[datanode.id].add(submittable.id) @@ -81,7 +81,7 @@ def __add(cls, submittable: Union["Scenario", "Sequence", "Task"], datanode: "Da cls.__publish_submittable_property_event(submittable, False) 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) @staticmethod diff --git a/taipy/core/_entity/submittable.py b/taipy/core/_entity/submittable.py index 09b6fdfd85..135a36d385 100644 --- a/taipy/core/_entity/submittable.py +++ b/taipy/core/_entity/submittable.py @@ -19,8 +19,7 @@ from ..common._utils import _Subscriber from ..data.data_node import DataNode 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 ..task.task import Task from ._dag import _DAG @@ -83,22 +82,22 @@ def get_intermediate(self) -> Set[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) - def is_ready_to_run(self) -> Reasons: + def is_ready_to_run(self) -> ReasonCollection: """Indicate if the entity is ready to be run. Returns: 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. """ - reason = Reasons(self._submittable_id) + reason_collection = ReasonCollection() for node in self.get_inputs(): 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: - 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]: """Return the set of data nodes of the submittable entity that are being edited. diff --git a/taipy/core/data/_data_manager.py b/taipy/core/data/_data_manager.py index 05890323fc..6a20c1e15c 100644 --- a/taipy/core/data/_data_manager.py +++ b/taipy/core/data/_data_manager.py @@ -22,8 +22,7 @@ from ..cycle.cycle_id import CycleId from ..exceptions.exceptions import InvalidDataNodeType 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 ..sequence.sequence_id import SequenceId from ._data_fs_repository import _DataFSRepository @@ -69,17 +68,17 @@ def _bulk_get_or_create( } @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) - reason = Reasons(config_id) + reason_collection = ReasonCollection() if config is not None: 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: - 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 def _create_and_set( diff --git a/taipy/core/data/data_node.py b/taipy/core/data/data_node.py index 32f7067a7d..8a0611caff 100644 --- a/taipy/core/data/data_node.py +++ b/taipy/core/data/data_node.py @@ -32,6 +32,7 @@ from ..exceptions.exceptions import DataNodeIsBeingEdited, NoData from ..job.job_id import JobId from ..notification.event import Event, EventEntityType, EventOperation, _make_event +from ..reason import DataNodeEditInProgress, DataNodeIsNotWritten from ._filter import _FilterDataNode from .data_node_id import DataNodeId, Edit from .operator import JoinOperator @@ -43,13 +44,13 @@ def _update_ready_for_reading(fct): def _recompute_is_ready_for_reading(dn: "DataNode", *args, **kwargs): fct(dn, *args, **kwargs) if dn._edit_in_progress: - _ReadyToRunProperty._add(dn, f"DataNode {dn.id} is being edited") + _ReadyToRunProperty._add(dn, DataNodeEditInProgress(dn.id)) else: - _ReadyToRunProperty._remove(dn, f"DataNode {dn.id} is being edited") + _ReadyToRunProperty._remove(dn, DataNodeEditInProgress(dn.id)) if not dn._last_edit_date: - _ReadyToRunProperty._add(dn, f"DataNode {dn.id} is not written") + _ReadyToRunProperty._add(dn, DataNodeIsNotWritten(dn.id)) else: - _ReadyToRunProperty._remove(dn, f"DataNode {dn.id} is not written") + _ReadyToRunProperty._remove(dn, DataNodeIsNotWritten(dn.id)) return _recompute_is_ready_for_reading @@ -396,7 +397,7 @@ def read(self) -> Any: return self.read_or_raise() except NoData: 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 diff --git a/taipy/core/reason/__init__.py b/taipy/core/reason/__init__.py index 347aaa33ff..d32434a44f 100644 --- a/taipy/core/reason/__init__.py +++ b/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 # 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 diff --git a/taipy/core/reason/_reason_factory.py b/taipy/core/reason/_reason_factory.py deleted file mode 100644 index c3fb327cd0..0000000000 --- a/taipy/core/reason/_reason_factory.py +++ /dev/null @@ -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' diff --git a/taipy/core/reason/reason.py b/taipy/core/reason/reason.py index 471fdebc09..f627e2a0b8 100644 --- a/taipy/core/reason/reason.py +++ b/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 # 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 - 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') diff --git a/taipy/core/reason/reason_collection.py b/taipy/core/reason/reason_collection.py new file mode 100644 index 0000000000..2f508cd407 --- /dev/null +++ b/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 "" diff --git a/taipy/core/scenario/_scenario_manager.py b/taipy/core/scenario/_scenario_manager.py index f850784625..efce92084a 100644 --- a/taipy/core/scenario/_scenario_manager.py +++ b/taipy/core/scenario/_scenario_manager.py @@ -39,8 +39,7 @@ from ..job._job_manager_factory import _JobManagerFactory from ..job.job import Job 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 import Submission from ..task._task_manager_factory import _TaskManagerFactory @@ -108,15 +107,15 @@ def __remove_subscriber(cls, callback, params, scenario: Scenario) -> None: ) @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) - reason = Reasons(config_id) + reason_collector = ReasonCollection() if config is not None: 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 def _create( @@ -202,15 +201,15 @@ def _create( return scenario @classmethod - def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> Reasons: + def _is_submittable(cls, scenario: Union[Scenario, ScenarioId]) -> ReasonCollection: if isinstance(scenario, str): scenario = cls._get(scenario) if not isinstance(scenario, 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() diff --git a/taipy/core/sequence/_sequence_manager.py b/taipy/core/sequence/_sequence_manager.py index 7387387ce2..98ca592797 100644 --- a/taipy/core/sequence/_sequence_manager.py +++ b/taipy/core/sequence/_sequence_manager.py @@ -29,8 +29,7 @@ from ..job.job import Job from ..notification import Event, EventEntityType, EventOperation, Notifier 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 import Scenario from ..scenario.scenario_id import ScenarioId @@ -342,15 +341,15 @@ def __remove_subscriber(cls, callback, params, sequence): Notifier.publish(_make_event(sequence, EventOperation.UPDATE, attribute_name="subscribers")) @classmethod - def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> Reasons: + def _is_submittable(cls, sequence: Union[Sequence, SequenceId]) -> ReasonCollection: if isinstance(sequence, str): sequence = cls._get(sequence) if not isinstance(sequence, 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() diff --git a/taipy/core/taipy.py b/taipy/core/taipy.py index fddcd3417d..a3f4376c44 100644 --- a/taipy/core/taipy.py +++ b/taipy/core/taipy.py @@ -45,8 +45,7 @@ from .job._job_manager_factory import _JobManagerFactory from .job.job import Job 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 import Scenario 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) -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. 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) if isinstance(entity, str) and entity.startswith(Task._ID_PREFIX): 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( @@ -880,7 +879,7 @@ def get_cycles() -> List[Cycle]: 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. If no config is provided, the function indicates if any scenario or data node config can be created. diff --git a/taipy/core/task/_task_manager.py b/taipy/core/task/_task_manager.py index c117b89056..e093d0067c 100644 --- a/taipy/core/task/_task_manager.py +++ b/taipy/core/task/_task_manager.py @@ -26,12 +26,7 @@ from ..data._data_manager_factory import _DataManagerFactory from ..exceptions.exceptions import NonExistingTask 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 ..sequence.sequence_id import SequenceId from ..submission.submission import Submission @@ -169,24 +164,24 @@ def _get_children_entity_ids(cls, task: Task) -> _EntityIds: return entity_ids @classmethod - def _is_submittable(cls, task: Union[Task, TaskId]) -> Reasons: + def _is_submittable(cls, task: Union[Task, TaskId]) -> ReasonCollection: if isinstance(task, str): task = cls._get(task) + + reason_collection = ReasonCollection() if not isinstance(task, 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: - reason = Reasons(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, _build_data_node_is_being_edited_reason(node.id)) + reason_collection._add_reason(node.id, DataNodeEditInProgress(node.id)) 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 def _submit( diff --git a/tests/core/_entity/test_ready_to_run_property.py b/tests/core/_entity/test_ready_to_run_property.py index 67d5634ec8..83c48a4dfd 100644 --- a/tests/core/_entity/test_ready_to_run_property.py +++ b/tests/core/_entity/test_ready_to_run_property.py @@ -14,7 +14,7 @@ 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.reason.reason import Reasons +from taipy.core.reason import DataNodeEditInProgress, DataNodeIsNotWritten, ReasonCollection 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 @@ -33,7 +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), Reasons) + assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection) 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) 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 @@ -61,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 isinstance(scenario_manager._is_submittable(scenario), Reasons) + assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection) 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 not dn_2.is_ready_for_reading 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 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 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 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] == { - 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." @@ -125,7 +125,7 @@ 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), Reasons) + assert isinstance(scenario_manager._is_submittable(scenario), ReasonCollection) assert scenario.id in _ReadyToRunProperty._submittable_id_datanodes 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 scenario.id in _ReadyToRunProperty._datanode_id_submittables[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 _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 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 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 assert scenario.id not in _ReadyToRunProperty._submittable_id_datanodes dn_1.lock_edit() 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 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) 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 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() 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 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") 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 dn.id not in _ReadyToRunProperty._datanode_id_submittables diff --git a/tests/core/common/test_reason.py b/tests/core/common/test_reason.py index 4ce21cf8fa..c00b15f9dc 100644 --- a/tests/core/common/test_reason.py +++ b/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 # 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(): - 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(): - 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(): - 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 diff --git a/tests/core/data/test_data_manager.py b/tests/core/data/test_data_manager.py index 7fb99200da..258bc1874b 100644 --- a/tests/core/data/test_data_manager.py +++ b/tests/core/data/test_data_manager.py @@ -24,6 +24,7 @@ from taipy.core.data.in_memory import InMemoryDataNode from taipy.core.data.pickle import PickleDataNode from taipy.core.exceptions.exceptions import InvalidDataNodeType, ModelNotFound +from taipy.core.reason import NotGlobalScope, WrongConfigType from tests.core.utils.named_temporary_file import NamedTemporaryFile @@ -61,11 +62,16 @@ def test_can_create(self): reasons = _DataManager._can_create(dn_config) 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) 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): dn_config = Config.configure_data_node(id="dn", foo="bar", name="acb") diff --git a/tests/core/scenario/test_scenario_manager.py b/tests/core/scenario/test_scenario_manager.py index d98b2e1206..83e0a33520 100644 --- a/tests/core/scenario/test_scenario_manager.py +++ b/tests/core/scenario/test_scenario_manager.py @@ -25,6 +25,7 @@ from taipy.core._version._version_manager import _VersionManager from taipy.core.common import _utils 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.data._data_manager import _DataManager from taipy.core.data.in_memory import InMemoryDataNode @@ -40,6 +41,7 @@ UnauthorizedTagError, ) 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_factory import _ScenarioManagerFactory from taipy.core.scenario.scenario import Scenario @@ -382,13 +384,15 @@ def test_can_create(): reasons = _ScenarioManager._can_create(task_config) 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): _ScenarioManager._create(task_config) reasons = _ScenarioManager._can_create(1) 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): _ScenarioManager._create(1) diff --git a/tests/gui_core/test_context_is_submitable.py b/tests/gui_core/test_context_is_submitable.py index 7e8a19a5f5..152ca23da9 100644 --- a/tests/gui_core/test_context_is_submitable.py +++ b/tests/gui_core/test_context_is_submitable.py @@ -14,7 +14,7 @@ from taipy.config.common.scope import Scope from taipy.core import Job, JobId, Scenario, Task 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 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}}) @@ -25,13 +25,13 @@ 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):