nova-mvvm 0.13.2__py3-none-any.whl → 0.15.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -83,6 +83,6 @@ def get_nested_pydantic_field(model: BaseModel, field_path: str) -> FieldInfo:
83
83
  if issubclass(type(getattr(current_model, field)), BaseModel):
84
84
  current_model = getattr(current_model, field)
85
85
  else:
86
- return current_model.model_fields[field]
86
+ return current_model.__class__.model_fields[field]
87
87
 
88
88
  raise Exception(f"Cannot find field {field_path}")
@@ -7,7 +7,7 @@ from pydantic import BaseModel, ValidationError
7
7
  from typing_extensions import override
8
8
 
9
9
  from .._internal.pydantic_utils import get_errored_fields_from_validation_error, get_updated_fields
10
- from .._internal.utils import check_binding, rsetattr
10
+ from .._internal.utils import check_binding, check_model_type, rsetattr
11
11
  from ..bindings_map import bindings_map
12
12
  from ..interface import Communicator, ConnectCallbackType
13
13
 
@@ -84,6 +84,8 @@ class PyQtCommunicator(Communicator):
84
84
  return None
85
85
 
86
86
  @override
87
- def update_in_view(self, value: Any) -> Any:
87
+ def update_in_view(self, value: Any) -> None:
88
88
  """Update a View (GUI) when called by a ViewModel."""
89
+ if issubclass(type(value), BaseModel):
90
+ check_model_type(self.viewmodel_linked_object, value)
89
91
  return self.pyqtobject.signal.emit(value)
@@ -1,6 +1,7 @@
1
1
  """Internal common functions tp be used within the package."""
2
2
 
3
3
  import re
4
+ from types import NoneType
4
5
  from typing import Any, Dict
5
6
 
6
7
  from nova.mvvm import bindings_map
@@ -98,3 +99,10 @@ def check_binding(linked_object: LinkedObjectType, name: str) -> None:
98
99
  for communicator in bindings_map.values():
99
100
  if communicator.viewmodel_linked_object and communicator.viewmodel_linked_object is linked_object:
100
101
  raise ValueError(f"cannot connect to binding {name}: object already connected")
102
+
103
+
104
+ def check_model_type(old_value: Any, new_value: Any) -> None:
105
+ old_type = type(old_value)
106
+ new_type = type(new_value)
107
+ if old_type is not NoneType and old_type is not new_type:
108
+ raise TypeError(f"update_in_view expected a value of type '{old_type}' but received '{new_type}'.")
@@ -9,7 +9,7 @@ from typing_extensions import override
9
9
 
10
10
  from .. import bindings_map
11
11
  from .._internal.pydantic_utils import get_errored_fields_from_validation_error, get_updated_fields
12
- from .._internal.utils import check_binding, rgetattr, rsetattr
12
+ from .._internal.utils import check_binding, check_model_type, rgetattr, rsetattr
13
13
  from ..interface import BindingInterface, ConnectCallbackType, Worker
14
14
 
15
15
 
@@ -114,6 +114,8 @@ class Communicator:
114
114
 
115
115
  # Update the view based on the provided value
116
116
  def update_in_view(self, value: Any) -> None:
117
+ if issubclass(type(value), BaseModel):
118
+ check_model_type(self.viewmodel_linked_object, value)
117
119
  if is_callable(self.connector):
118
120
  cast(Callable, self.connector)(value)
119
121
  elif self.viewmodel_linked_object:
@@ -12,6 +12,8 @@ from ._internal.pydantic_utils import get_nested_pydantic_field
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
15
+ ERROR_FIELD_NAME = "pydantic_errors"
16
+
15
17
 
16
18
  def get_field_info(field_name: str) -> FieldInfo:
17
19
  """
@@ -10,7 +10,14 @@ from trame_server.state import State
10
10
  from typing_extensions import override
11
11
 
12
12
  from .._internal.pydantic_utils import get_errored_fields_from_validation_error, get_updated_fields
13
- from .._internal.utils import check_binding, normalize_field_name, rget_list_of_fields, rgetattr, rsetattr
13
+ from .._internal.utils import (
14
+ check_binding,
15
+ check_model_type,
16
+ normalize_field_name,
17
+ rget_list_of_fields,
18
+ rgetattr,
19
+ rsetattr,
20
+ )
14
21
  from ..bindings_map import bindings_map
15
22
  from ..interface import (
16
23
  BindingInterface,
@@ -21,6 +28,7 @@ from ..interface import (
21
28
  LinkedObjectType,
22
29
  Worker,
23
30
  )
31
+ from ..pydantic_utils import ERROR_FIELD_NAME
24
32
  from .trame_worker import TrameWorker
25
33
 
26
34
 
@@ -132,6 +140,8 @@ class CallBackConnection:
132
140
  self.viewmodel_callback_after_update({"updated": updates, "errored": errors, "error": None})
133
141
 
134
142
  def update_in_view(self, value: Any) -> None:
143
+ if issubclass(type(value), BaseModel):
144
+ check_model_type(self.viewmodel_linked_object, value)
135
145
  self.callback(value)
136
146
 
137
147
  def get_callback(self) -> ConnectCallbackType:
@@ -167,12 +177,21 @@ class StateConnection:
167
177
  return update
168
178
 
169
179
  def _set_variable_in_state(self, name_in_state: str, value: Any) -> None:
180
+ if "." in name_in_state:
181
+ base_name, name_in_state = name_in_state.split(".", maxsplit=1)
182
+ if self.state[base_name] is None:
183
+ self.state[base_name] = {}
184
+ state_obj = self.state[base_name]
185
+ else:
186
+ base_name = name_in_state
187
+ state_obj = self.state
188
+
170
189
  if is_async():
171
190
  with self.state:
172
- self.state[name_in_state] = value
191
+ state_obj[name_in_state] = value
173
192
  self.state.dirty(name_in_state)
174
193
  else:
175
- self.state[name_in_state] = value
194
+ state_obj[name_in_state] = value
176
195
  self.state.dirty(name_in_state)
177
196
 
178
197
  def _get_name_in_state(self, attribute_name: str) -> str:
@@ -198,6 +217,9 @@ class StateConnection:
198
217
  name_in_state = self._get_name_in_state(attribute_name)
199
218
  self.state.setdefault(name_in_state, None)
200
219
 
220
+ # Set the initial error state.
221
+ self._set_variable_in_state(f"{state_variable_name}.{ERROR_FIELD_NAME}", [])
222
+
201
223
  # this updates ViewModel on state change
202
224
  if self.viewmodel_linked_object:
203
225
  if self.linked_object_attributes:
@@ -225,6 +247,7 @@ class StateConnection:
225
247
  updated = False
226
248
  except ValidationError as e:
227
249
  errors = get_errored_fields_from_validation_error(e)
250
+ self._set_variable_in_state(f"{state_variable_name}.{ERROR_FIELD_NAME}", errors)
228
251
  error = e
229
252
  updated = True
230
253
  self.has_errors = True
@@ -239,19 +262,23 @@ class StateConnection:
239
262
  if self.has_errors and not errors:
240
263
  updated = True
241
264
  self.has_errors = False
265
+ self._set_variable_in_state(f"{state_variable_name}.{ERROR_FIELD_NAME}", [])
242
266
  if updated:
243
267
  await self._handle_callback({"updated": updates, "errored": errors, "error": error})
244
268
 
245
269
  def update_in_view(self, value: Any) -> None:
246
270
  if issubclass(type(value), BaseModel):
271
+ check_model_type(self.viewmodel_linked_object, value)
247
272
  value = value.model_dump()
248
273
  if self.linked_object_attributes:
249
274
  for attribute_name in self.linked_object_attributes:
250
275
  name_in_state = self._get_name_in_state(attribute_name)
251
276
  value_to_change = rgetattr(value, attribute_name)
252
277
  self._set_variable_in_state(name_in_state, value_to_change)
278
+ self._set_variable_in_state(f"{name_in_state}.{ERROR_FIELD_NAME}", [])
253
279
  elif self.state_variable_name:
254
280
  self._set_variable_in_state(self.state_variable_name, value)
281
+ self._set_variable_in_state(f"{self.state_variable_name}.{ERROR_FIELD_NAME}", [])
255
282
 
256
283
  def get_callback(self) -> ConnectCallbackType:
257
284
  return None
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nova-mvvm
3
- Version: 0.13.2
3
+ Version: 0.15.0
4
4
  Summary: A Python Package for Model-View-ViewModel pattern
5
- Author-email: "Yakubov, Sergey" <yakubovs@ornl.gov>
5
+ Author-email: Sergey Yakubov <yakubovs@ornl.gov>, John Duggan <dugganjw@ornl.gov>, Greg Watson <watsongr@ornl.gov>
6
6
  License-Expression: MIT
7
7
  License-File: LICENSE
8
8
  Keywords: MVVM,python
@@ -2,12 +2,12 @@ nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
2
2
  nova/mvvm/__init__.py,sha256=fo1bT27jjUYzVoFJXuwokXcyFtOYj8eQ_BHkPgtI4Rg,149
3
3
  nova/mvvm/bindings_map.py,sha256=SIVNPGHxDf91P9UxcPmuAHx1GNxZk66JUc-fYPcp-kc,417
4
4
  nova/mvvm/interface.py,sha256=l4l339BiB46s6KX-2RH1Q3Hxc8pKYdCvCOkhse4zOkw,5812
5
- nova/mvvm/pydantic_utils.py,sha256=kQ17-Thp_4FMQca6nJ_UNWbctssqHGY3FtZolUIZGQ0,3191
6
- nova/mvvm/_internal/pydantic_utils.py,sha256=kHZLR5Noc581kz-OYRO6EW-jyjB443BF_wVoXY5jBRA,2937
7
- nova/mvvm/_internal/pyqt_communicator.py,sha256=kcEaXCjJZXymNBM-8x5Yk22SUQFrQ7c5yV1-GDYf3DM,3797
8
- nova/mvvm/_internal/utils.py,sha256=2YDUvAmJjjb89ROE4n4GWD4Qqz-rRqVP4ICDGnIcFNc,3303
5
+ nova/mvvm/pydantic_utils.py,sha256=t8niOvGoA87TUjT4adbgMdoFL9mDaNjLkV1xXyWlDK8,3229
6
+ nova/mvvm/_internal/pydantic_utils.py,sha256=anYYyB1Bkg8GgWB3Jn-6iRit0PYR-_2w3paKsD9z4sY,2947
7
+ nova/mvvm/_internal/pyqt_communicator.py,sha256=G_Srttxn7EEwz9EageFxnOgg_P-gfPPhNpoR55btHd0,3929
8
+ nova/mvvm/_internal/utils.py,sha256=LpZ3LALcB9BzG1yIh0Qt0kBXGdctacDMVmSm8H8Py1c,3626
9
9
  nova/mvvm/panel_binding/__init__.py,sha256=HHDlUyP-LuRePcHS1s8tq3niuG3f3n8gdSRb7Rwtc4o,100
10
- nova/mvvm/panel_binding/binding.py,sha256=Fz_t2XHoU8hfbf-nDDypk4V_KHH7T7OqYFCl65k4XEE,6352
10
+ nova/mvvm/panel_binding/binding.py,sha256=rLkxnqjLpZvvRHJVx8PLfJpLSEg3Q0FSRPov81o8e6Y,6483
11
11
  nova/mvvm/pyqt5_binding/__init__.py,sha256=JbKOFJNIObJReotRHlN5-vi7oSMyc_v8K_uHjeJLcHg,62
12
12
  nova/mvvm/pyqt5_binding/binding.py,sha256=Nki03KgafysfJFQ86evYvL8lprbrYNajFNgfihk7KA4,1462
13
13
  nova/mvvm/pyqt5_binding/pyqt5_worker.py,sha256=bf0Yj2cnX3xljFHQmWy6B9bCildhNSVEDXSqahprCXU,2133
@@ -15,9 +15,9 @@ nova/mvvm/pyqt6_binding/__init__.py,sha256=plms0W4kJjFpZReGOC7lE3SYX1ceGTLbdAJ1G
15
15
  nova/mvvm/pyqt6_binding/binding.py,sha256=uZNHx23Pf9DwGf3qKfQdmjOhmaXcppNd8pjkh363WP4,1365
16
16
  nova/mvvm/pyqt6_binding/pyqt6_worker.py,sha256=y8g_mfPC1xIcpyKq7ZZqxrn9Txp5gPJrsBjAwS5FHhY,2133
17
17
  nova/mvvm/trame_binding/__init__.py,sha256=uTdEW9VxtVubSbTLpoD3pC8a-KMgbKkZFlRbucvuQSE,62
18
- nova/mvvm/trame_binding/binding.py,sha256=H5iTOY5FwN1UZnxPXiYmOt36hv88e_4XzyYF1NK6KH4,12255
18
+ nova/mvvm/trame_binding/binding.py,sha256=hKIeIUAp9Zzc4UtGtzD6UBwQltfh5i79pEzpWHLRS7E,13369
19
19
  nova/mvvm/trame_binding/trame_worker.py,sha256=JmrneFU11Hi8TtY558R3yIWW8UGiUeVNO7HvM1uWTVs,3778
20
- nova_mvvm-0.13.2.dist-info/METADATA,sha256=A5hCBiCeMxcBRB60XLsiIXt5UCjJMstF7C2eKdyVkuk,1027
21
- nova_mvvm-0.13.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
22
- nova_mvvm-0.13.2.dist-info/licenses/LICENSE,sha256=MOqZ8tPMKy8ZETJ2-HEvFTZ7dYNlg3gXmBkV-Y9i8bw,1061
23
- nova_mvvm-0.13.2.dist-info/RECORD,,
20
+ nova_mvvm-0.15.0.dist-info/METADATA,sha256=BfmKRWdHZ0gdiOvQXJQ_4YcuWxAYv-LuKQ6rHBhF7Go,1090
21
+ nova_mvvm-0.15.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
22
+ nova_mvvm-0.15.0.dist-info/licenses/LICENSE,sha256=MOqZ8tPMKy8ZETJ2-HEvFTZ7dYNlg3gXmBkV-Y9i8bw,1061
23
+ nova_mvvm-0.15.0.dist-info/RECORD,,