Loading poetry.lock +737 −705 File changed.Preview size limit exceeded, changes collapsed. Show changes pyproject.toml +1 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ trame = "^3.6.3" pyqt5 = { version = "^5.15.11", optional = true } pyqt6 = { version = "^6.7.0", optional = true } panel = { version = "^1.4.4", optional = true } deepdiff = "^8.0.1" [tool.poetry.extras] pyqt5 = ["pyqt5"] Loading src/nova/mvvm/interface.py +2 −2 Original line number Diff line number Diff line """Abstract interfaces and type definitions.""" from abc import ABC, abstractmethod from typing import Any, Callable, Optional, Union from typing import Any, Callable, Dict, Optional, Union LinkedObjectType = Optional[Any] LinkedObjectAttributesType = Optional[list[str]] CallbackAfterUpdateType = Optional[Callable[[Optional[str]], None]] CallbackAfterUpdateType = Optional[Callable[[Dict[str, Any]], None]] ConnectCallbackType = Union[None, Callable[[Any, Optional[str]], None]] Loading src/nova/mvvm/pydantic_utils.py +38 −1 Original line number Diff line number Diff line Loading @@ -2,8 +2,9 @@ import logging import re from typing import Any from typing import Any, Tuple from deepdiff import DeepDiff from pydantic import BaseModel, ValidationError from pydantic.fields import FieldInfo Loading @@ -12,6 +13,26 @@ from nova.mvvm import bindings_map logger = logging.getLogger(__name__) def _format_tuple(input_tuple: Tuple) -> str: res = "" for item in input_tuple: if isinstance(item, int): formatted = f"[{item}]" elif isinstance(item, str): formatted = f".{item}" if res else item else: formatted = str(item) res += formatted return res def get_errored_fields_from_validation_error(e: ValidationError) -> list[str]: res = [] for error in e.errors(): res.append(_format_tuple(error["loc"])) return res def get_nested_pydantic_field(model: BaseModel, field_path: str) -> FieldInfo: fields = field_path.split(".") current_model: Any = model Loading Loading @@ -73,3 +94,19 @@ def validate_pydantic_parameter(name: str, value: Any) -> str | None: ): return error["msg"] return None def _remove_brackets_suffix(s: str) -> str: return re.sub(r"\[\d+\]$", "", s) def get_updated_fields(old: BaseModel, new: BaseModel) -> list[str]: diff = DeepDiff(old, new) updates: list[str] = [] if "values_changed" in diff: updates = [k.removeprefix("root.") for k in diff["values_changed"].keys()] for item in ["iterable_item_added", "iterable_item_removed"]: if item in diff: updates += [_remove_brackets_suffix(k.removeprefix("root.")) for k in diff[item].keys()] return updates src/nova/mvvm/pyqt_binding/binding.py +18 −8 Original line number Diff line number Diff line Loading @@ -3,9 +3,10 @@ import os from typing import Any, Callable, Optional from pydantic import BaseModel from pydantic import BaseModel, ValidationError from ..bindings_map import update_bindings_map from ..pydantic_utils import get_errored_fields_from_validation_error, get_updated_fields from ..utils import rsetattr if os.environ.get("QT_API", None) == "pyqt5": Loading Loading @@ -49,16 +50,26 @@ class PyQtCommunicator(QObject): def _update_viewmodel_callback(self, key: Optional[str] = None, value: Any = None) -> None: if issubclass(type(self.viewmodel_linked_object), BaseModel): updates: list[str] = [] errors: list[str] = [] error: Any = None updated = True model = self.viewmodel_linked_object.copy(deep=True) if self.prefix and key: key = key.removeprefix(f"{self.prefix}.") rsetattr(model, key or "", value) try: new_model = model.__class__(**model.model_dump(warnings=False)) for f, v in new_model: setattr(self.viewmodel_linked_object, f, v) except Exception: pass if new_model != self.viewmodel_linked_object: updates = get_updated_fields(self.viewmodel_linked_object, new_model) for field, value in new_model: setattr(self.viewmodel_linked_object, field, value) else: updated = False except ValidationError as e: errors = get_errored_fields_from_validation_error(e) error = e updated = True elif isinstance(self.viewmodel_linked_object, dict): self.viewmodel_linked_object.update({key: value}) elif is_callable(self.viewmodel_linked_object): Loading @@ -67,9 +78,8 @@ class PyQtCommunicator(QObject): rsetattr(self.viewmodel_linked_object, key or "", value) else: raise Exception("Cannot update", self.viewmodel_linked_object) if self.callback_after_update: self.callback_after_update(key) 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: # connect should be called from the View side to connect a Loading Loading
pyproject.toml +1 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ trame = "^3.6.3" pyqt5 = { version = "^5.15.11", optional = true } pyqt6 = { version = "^6.7.0", optional = true } panel = { version = "^1.4.4", optional = true } deepdiff = "^8.0.1" [tool.poetry.extras] pyqt5 = ["pyqt5"] Loading
src/nova/mvvm/interface.py +2 −2 Original line number Diff line number Diff line """Abstract interfaces and type definitions.""" from abc import ABC, abstractmethod from typing import Any, Callable, Optional, Union from typing import Any, Callable, Dict, Optional, Union LinkedObjectType = Optional[Any] LinkedObjectAttributesType = Optional[list[str]] CallbackAfterUpdateType = Optional[Callable[[Optional[str]], None]] CallbackAfterUpdateType = Optional[Callable[[Dict[str, Any]], None]] ConnectCallbackType = Union[None, Callable[[Any, Optional[str]], None]] Loading
src/nova/mvvm/pydantic_utils.py +38 −1 Original line number Diff line number Diff line Loading @@ -2,8 +2,9 @@ import logging import re from typing import Any from typing import Any, Tuple from deepdiff import DeepDiff from pydantic import BaseModel, ValidationError from pydantic.fields import FieldInfo Loading @@ -12,6 +13,26 @@ from nova.mvvm import bindings_map logger = logging.getLogger(__name__) def _format_tuple(input_tuple: Tuple) -> str: res = "" for item in input_tuple: if isinstance(item, int): formatted = f"[{item}]" elif isinstance(item, str): formatted = f".{item}" if res else item else: formatted = str(item) res += formatted return res def get_errored_fields_from_validation_error(e: ValidationError) -> list[str]: res = [] for error in e.errors(): res.append(_format_tuple(error["loc"])) return res def get_nested_pydantic_field(model: BaseModel, field_path: str) -> FieldInfo: fields = field_path.split(".") current_model: Any = model Loading Loading @@ -73,3 +94,19 @@ def validate_pydantic_parameter(name: str, value: Any) -> str | None: ): return error["msg"] return None def _remove_brackets_suffix(s: str) -> str: return re.sub(r"\[\d+\]$", "", s) def get_updated_fields(old: BaseModel, new: BaseModel) -> list[str]: diff = DeepDiff(old, new) updates: list[str] = [] if "values_changed" in diff: updates = [k.removeprefix("root.") for k in diff["values_changed"].keys()] for item in ["iterable_item_added", "iterable_item_removed"]: if item in diff: updates += [_remove_brackets_suffix(k.removeprefix("root.")) for k in diff[item].keys()] return updates
src/nova/mvvm/pyqt_binding/binding.py +18 −8 Original line number Diff line number Diff line Loading @@ -3,9 +3,10 @@ import os from typing import Any, Callable, Optional from pydantic import BaseModel from pydantic import BaseModel, ValidationError from ..bindings_map import update_bindings_map from ..pydantic_utils import get_errored_fields_from_validation_error, get_updated_fields from ..utils import rsetattr if os.environ.get("QT_API", None) == "pyqt5": Loading Loading @@ -49,16 +50,26 @@ class PyQtCommunicator(QObject): def _update_viewmodel_callback(self, key: Optional[str] = None, value: Any = None) -> None: if issubclass(type(self.viewmodel_linked_object), BaseModel): updates: list[str] = [] errors: list[str] = [] error: Any = None updated = True model = self.viewmodel_linked_object.copy(deep=True) if self.prefix and key: key = key.removeprefix(f"{self.prefix}.") rsetattr(model, key or "", value) try: new_model = model.__class__(**model.model_dump(warnings=False)) for f, v in new_model: setattr(self.viewmodel_linked_object, f, v) except Exception: pass if new_model != self.viewmodel_linked_object: updates = get_updated_fields(self.viewmodel_linked_object, new_model) for field, value in new_model: setattr(self.viewmodel_linked_object, field, value) else: updated = False except ValidationError as e: errors = get_errored_fields_from_validation_error(e) error = e updated = True elif isinstance(self.viewmodel_linked_object, dict): self.viewmodel_linked_object.update({key: value}) elif is_callable(self.viewmodel_linked_object): Loading @@ -67,9 +78,8 @@ class PyQtCommunicator(QObject): rsetattr(self.viewmodel_linked_object, key or "", value) else: raise Exception("Cannot update", self.viewmodel_linked_object) if self.callback_after_update: self.callback_after_update(key) 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: # connect should be called from the View side to connect a Loading