Skip to content

Commit

Permalink
Merge pull request #1459 from Avaiga/feature/reasons-refacto
Browse files Browse the repository at this point in the history
Reason class refactoring
  • Loading branch information
toan-quach authored Jul 11, 2024
2 parents a2b94cf + f831922 commit b2cd473
Show file tree
Hide file tree
Showing 17 changed files with 308 additions and 190 deletions.
12 changes: 6 additions & 6 deletions taipy/core/_entity/_ready_to_run_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
13 changes: 6 additions & 7 deletions taipy/core/_entity/submittable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
13 changes: 6 additions & 7 deletions taipy/core/data/_data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
11 changes: 6 additions & 5 deletions taipy/core/data/data_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
10 changes: 9 additions & 1 deletion taipy/core/reason/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 0 additions & 37 deletions taipy/core/reason/_reason_factory.py

This file was deleted.

128 changes: 106 additions & 22 deletions taipy/core/reason/reason.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Loading

0 comments on commit b2cd473

Please sign in to comment.