Loading pyproject.toml +1 −1 Original line number Diff line number Diff line [tool.poetry] name = "nova-mvvm" version = "0.7.2" version = "0.8.0" description = "A Python Package for Model-View-ViewModel pattern" authors = ["Yakubov, Sergey <yakubovs@ornl.gov>"] readme = "README.md" Loading src/nova/mvvm/_internal/utils.py +11 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,9 @@ import re from typing import Any, Dict from nova.mvvm import bindings_map from nova.mvvm.interface import LinkedObjectType def normalize_field_name(field: str) -> str: return field.replace(".", "_").replace("[", "_").replace("]", "") Loading Loading @@ -87,3 +90,11 @@ def rgetdictvalue(obj: Dict[str, Any], field: str) -> Any: for index in indices: obj = obj[index] return obj def check_binding(linked_object: LinkedObjectType, name: str) -> None: if name in bindings_map: raise ValueError(f"cannot connect to binding {name}: name already used") for communicator in bindings_map.values(): if communicator.viewmodel_linked_object and communicator.viewmodel_linked_object == linked_object: raise ValueError(f"cannot connect to binding {name}: object already connected") src/nova/mvvm/interface.py +1 −1 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ class Communicator(ABC): """ @abstractmethod def connect(self, connector: Any = None) -> ConnectCallbackType: def connect(self, *args: Any) -> ConnectCallbackType: """ Connect a GUI element to a linked object. Loading src/nova/mvvm/pyqt_binding/binding.py +21 −9 Original line number Diff line number Diff line """Binding module for PyQt framework.""" import os from typing import Any, Callable, Optional from typing import Any, Optional from pydantic import BaseModel, ValidationError from typing_extensions import override from .._internal.pydantic_utils import get_errored_fields_from_validation_error, get_updated_fields from .._internal.utils import rsetattr from .._internal.utils import check_binding, rsetattr from ..bindings_map import bindings_map if os.environ.get("QT_API", None) == "pyqt5": Loading @@ -24,18 +25,22 @@ else: import inspect from ..interface import BindingInterface from ..interface import BindingInterface, Communicator, ConnectCallbackType def is_callable(var: Any) -> bool: return inspect.isfunction(var) or inspect.ismethod(var) class PyQtCommunicator(QObject): """Communicator class, that provides methods required for binding to communicate between ViewModel and View.""" class PyQtObject(QObject): """PyQt object class.""" signal = pyqtSignal(object) class PyQtCommunicator(Communicator): """Communicator class, that provides methods required for binding to communicate between ViewModel and View.""" def __init__( self, viewmodel_linked_object: Any = None, Loading @@ -43,6 +48,7 @@ class PyQtCommunicator(QObject): callback_after_update: Any = None, ) -> None: super().__init__() self.pyqtobject = PyQtObject() self.viewmodel_linked_object = viewmodel_linked_object self.linked_object_attributes = linked_object_attributes self.callback_after_update = callback_after_update Loading Loading @@ -77,25 +83,31 @@ class PyQtCommunicator(QObject): elif isinstance(self.viewmodel_linked_object, object): rsetattr(self.viewmodel_linked_object, key or "", value) else: raise Exception("Cannot update", self.viewmodel_linked_object) raise ValueError("Cannot update", self.viewmodel_linked_object) if updated and self.callback_after_update: self.callback_after_update({"updated": updates, "errored": errors, "error": error}) def connect(self, name: str, callback: Callable) -> Any: @override def connect(self, name: str, connector: Any) -> ConnectCallbackType: # connect should be called from the View side to connect a # GUI element (via a function to change GUI element that is passed to the connect call) # and a linked_object (passed during bind creation from ViewModel side) if not is_callable(connector): raise ValueError("connector should be a callable type") check_binding(self.viewmodel_linked_object, name) bindings_map[name] = self self.prefix = name self.signal.connect(callback) self.pyqtobject.signal.connect(connector) if self.viewmodel_linked_object: return self._update_viewmodel_callback else: return None @override def update_in_view(self, value: Any) -> Any: """Update a View (GUI) when called by a ViewModel.""" return self.signal.emit(value) return self.pyqtobject.signal.emit(value) class PyQtBinding(BindingInterface): Loading src/nova/mvvm/trame_binding/binding.py +3 −1 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ from trame_server.state import State from typing_extensions import override from .._internal.pydantic_utils import get_errored_fields_from_validation_error, get_updated_fields from .._internal.utils import normalize_field_name, rget_list_of_fields, rgetattr, rsetattr from .._internal.utils import check_binding, normalize_field_name, rget_list_of_fields, rgetattr, rsetattr from ..bindings_map import bindings_map from ..interface import ( BindingInterface, Loading Loading @@ -72,10 +72,12 @@ class TrameCommunicator(Communicator): else: connector = str(connector) if connector else None if connector: check_binding(self.viewmodel_linked_object, connector) bindings_map[connector] = self self.connection = StateConnection(self, connector) return self.connection.get_callback() @override def update_in_view(self, value: Any) -> None: self.connection.update_in_view(value) Loading Loading
pyproject.toml +1 −1 Original line number Diff line number Diff line [tool.poetry] name = "nova-mvvm" version = "0.7.2" version = "0.8.0" description = "A Python Package for Model-View-ViewModel pattern" authors = ["Yakubov, Sergey <yakubovs@ornl.gov>"] readme = "README.md" Loading
src/nova/mvvm/_internal/utils.py +11 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,9 @@ import re from typing import Any, Dict from nova.mvvm import bindings_map from nova.mvvm.interface import LinkedObjectType def normalize_field_name(field: str) -> str: return field.replace(".", "_").replace("[", "_").replace("]", "") Loading Loading @@ -87,3 +90,11 @@ def rgetdictvalue(obj: Dict[str, Any], field: str) -> Any: for index in indices: obj = obj[index] return obj def check_binding(linked_object: LinkedObjectType, name: str) -> None: if name in bindings_map: raise ValueError(f"cannot connect to binding {name}: name already used") for communicator in bindings_map.values(): if communicator.viewmodel_linked_object and communicator.viewmodel_linked_object == linked_object: raise ValueError(f"cannot connect to binding {name}: object already connected")
src/nova/mvvm/interface.py +1 −1 Original line number Diff line number Diff line Loading @@ -18,7 +18,7 @@ class Communicator(ABC): """ @abstractmethod def connect(self, connector: Any = None) -> ConnectCallbackType: def connect(self, *args: Any) -> ConnectCallbackType: """ Connect a GUI element to a linked object. Loading
src/nova/mvvm/pyqt_binding/binding.py +21 −9 Original line number Diff line number Diff line """Binding module for PyQt framework.""" import os from typing import Any, Callable, Optional from typing import Any, Optional from pydantic import BaseModel, ValidationError from typing_extensions import override from .._internal.pydantic_utils import get_errored_fields_from_validation_error, get_updated_fields from .._internal.utils import rsetattr from .._internal.utils import check_binding, rsetattr from ..bindings_map import bindings_map if os.environ.get("QT_API", None) == "pyqt5": Loading @@ -24,18 +25,22 @@ else: import inspect from ..interface import BindingInterface from ..interface import BindingInterface, Communicator, ConnectCallbackType def is_callable(var: Any) -> bool: return inspect.isfunction(var) or inspect.ismethod(var) class PyQtCommunicator(QObject): """Communicator class, that provides methods required for binding to communicate between ViewModel and View.""" class PyQtObject(QObject): """PyQt object class.""" signal = pyqtSignal(object) class PyQtCommunicator(Communicator): """Communicator class, that provides methods required for binding to communicate between ViewModel and View.""" def __init__( self, viewmodel_linked_object: Any = None, Loading @@ -43,6 +48,7 @@ class PyQtCommunicator(QObject): callback_after_update: Any = None, ) -> None: super().__init__() self.pyqtobject = PyQtObject() self.viewmodel_linked_object = viewmodel_linked_object self.linked_object_attributes = linked_object_attributes self.callback_after_update = callback_after_update Loading Loading @@ -77,25 +83,31 @@ class PyQtCommunicator(QObject): elif isinstance(self.viewmodel_linked_object, object): rsetattr(self.viewmodel_linked_object, key or "", value) else: raise Exception("Cannot update", self.viewmodel_linked_object) raise ValueError("Cannot update", self.viewmodel_linked_object) if updated and self.callback_after_update: self.callback_after_update({"updated": updates, "errored": errors, "error": error}) def connect(self, name: str, callback: Callable) -> Any: @override def connect(self, name: str, connector: Any) -> ConnectCallbackType: # connect should be called from the View side to connect a # GUI element (via a function to change GUI element that is passed to the connect call) # and a linked_object (passed during bind creation from ViewModel side) if not is_callable(connector): raise ValueError("connector should be a callable type") check_binding(self.viewmodel_linked_object, name) bindings_map[name] = self self.prefix = name self.signal.connect(callback) self.pyqtobject.signal.connect(connector) if self.viewmodel_linked_object: return self._update_viewmodel_callback else: return None @override def update_in_view(self, value: Any) -> Any: """Update a View (GUI) when called by a ViewModel.""" return self.signal.emit(value) return self.pyqtobject.signal.emit(value) class PyQtBinding(BindingInterface): Loading
src/nova/mvvm/trame_binding/binding.py +3 −1 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ from trame_server.state import State from typing_extensions import override from .._internal.pydantic_utils import get_errored_fields_from_validation_error, get_updated_fields from .._internal.utils import normalize_field_name, rget_list_of_fields, rgetattr, rsetattr from .._internal.utils import check_binding, normalize_field_name, rget_list_of_fields, rgetattr, rsetattr from ..bindings_map import bindings_map from ..interface import ( BindingInterface, Loading Loading @@ -72,10 +72,12 @@ class TrameCommunicator(Communicator): else: connector = str(connector) if connector else None if connector: check_binding(self.viewmodel_linked_object, connector) bindings_map[connector] = self self.connection = StateConnection(self, connector) return self.connection.get_callback() @override def update_in_view(self, value: Any) -> None: self.connection.update_in_view(value) Loading