Commit 8bceccc8 authored by Yakubov, Sergey's avatar Yakubov, Sergey
Browse files

Merge branch '16-deal-with-double-binding-situation' into 'main'

Deal with double binding situation

Closes #16

See merge request ndip/public-packages/nova-mvvm!17
parents 8b274dcb 4292c2db
Loading
Loading
Loading
Loading
Loading
+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"
+11 −0
Original line number Diff line number Diff line
@@ -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("]", "")
@@ -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")
+1 −1
Original line number Diff line number Diff line
@@ -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.

+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":
@@ -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,
@@ -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
@@ -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):
+3 −1
Original line number Diff line number Diff line
@@ -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,
@@ -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