9
9
from deepdiff import DeepDiff , Delta
10
10
from deepdiff .operator import BaseOperator
11
11
from deepdiff .path import parse_path
12
+ from pydantic import BaseModel
12
13
13
14
from mesop .components .uploader .uploaded_file import UploadedFile
14
15
from mesop .exceptions import MesopDeveloperException , MesopException
15
16
16
17
_PANDAS_OBJECT_KEY = "__pandas.DataFrame__"
18
+ _PYDANTIC_OBJECT_KEY = "__pydantic.BaseModel__"
17
19
_DATETIME_OBJECT_KEY = "__datetime.datetime__"
18
20
_BYTES_OBJECT_KEY = "__python.bytes__"
19
21
_SET_OBJECT_KEY = "__python.set__"
20
22
_UPLOADED_FILE_OBJECT_KEY = "__mesop.UploadedFile__"
21
23
_DIFF_ACTION_DATA_FRAME_CHANGED = "data_frame_changed"
22
- _DIFF_ACTION_UPLOADED_FILE_CHANGED = "mesop_uploaded_file_changed "
24
+ _DIFF_ACTION_EQUALITY_CHANGED = "mesop_equality_changed "
23
25
24
26
C = TypeVar ("C" )
25
27
@@ -36,6 +38,8 @@ def _check_has_pandas():
36
38
37
39
_has_pandas = _check_has_pandas ()
38
40
41
+ pydantic_model_cache = {}
42
+
39
43
40
44
def dataclass_with_defaults (cls : Type [C ]) -> Type [C ]:
41
45
"""
@@ -64,6 +68,14 @@ def dataclass_with_defaults(cls: Type[C]) -> Type[C]:
64
68
65
69
annotations = get_type_hints (cls )
66
70
for name , type_hint in annotations .items ():
71
+ if (
72
+ isinstance (type_hint , type )
73
+ and has_parent (type_hint )
74
+ and issubclass (type_hint , BaseModel )
75
+ ):
76
+ pydantic_model_cache [(type_hint .__module__ , type_hint .__qualname__ )] = (
77
+ type_hint
78
+ )
67
79
if name not in cls .__dict__ : # Skip if default already set
68
80
if type_hint == int :
69
81
setattr (cls , name , field (default = 0 ))
@@ -187,6 +199,15 @@ def default(self, obj):
187
199
}
188
200
}
189
201
202
+ if isinstance (obj , BaseModel ):
203
+ return {
204
+ _PYDANTIC_OBJECT_KEY : {
205
+ "json" : obj .model_dump_json (),
206
+ "module" : obj .__class__ .__module__ ,
207
+ "qualname" : obj .__class__ .__qualname__ ,
208
+ }
209
+ }
210
+
190
211
if isinstance (obj , datetime ):
191
212
return {_DATETIME_OBJECT_KEY : obj .isoformat ()}
192
213
@@ -221,6 +242,18 @@ def decode_mesop_json_state_hook(dct):
221
242
if _PANDAS_OBJECT_KEY in dct :
222
243
return pd .read_json (StringIO (dct [_PANDAS_OBJECT_KEY ]), orient = "table" )
223
244
245
+ if _PYDANTIC_OBJECT_KEY in dct :
246
+ cache_key = (
247
+ dct [_PYDANTIC_OBJECT_KEY ]["module" ],
248
+ dct [_PYDANTIC_OBJECT_KEY ]["qualname" ],
249
+ )
250
+ if cache_key not in pydantic_model_cache :
251
+ raise MesopException (
252
+ f"Tried to deserialize Pydantic model, but it's not in the cache: { cache_key } "
253
+ )
254
+ model_class = pydantic_model_cache [cache_key ]
255
+ return model_class .model_validate_json (dct [_PYDANTIC_OBJECT_KEY ]["json" ])
256
+
224
257
if _DATETIME_OBJECT_KEY in dct :
225
258
return datetime .fromisoformat (dct [_DATETIME_OBJECT_KEY ])
226
259
@@ -269,25 +302,22 @@ def give_up_diffing(self, level, diff_instance) -> bool:
269
302
return True
270
303
271
304
272
- class UploadedFileOperator (BaseOperator ):
273
- """Custom operator to detect changes in UploadedFile class .
305
+ class EqualityOperator (BaseOperator ):
306
+ """Custom operator to detect changes with direct equality .
274
307
275
308
DeepDiff does not diff the UploadedFile class correctly, so we will just use a normal
276
309
equality check, rather than diffing further into the io.BytesIO parent class.
277
-
278
- This class could probably be made more generic to handle other classes where we want
279
- to diff using equality checks.
280
310
"""
281
311
282
312
def match (self , level ) -> bool :
283
- return isinstance (level .t1 , UploadedFile ) and isinstance (
284
- level .t2 , UploadedFile
313
+ return isinstance (level .t1 , ( UploadedFile , BaseModel ) ) and isinstance (
314
+ level .t2 , ( UploadedFile , BaseModel )
285
315
)
286
316
287
317
def give_up_diffing (self , level , diff_instance ) -> bool :
288
318
if level .t1 != level .t2 :
289
319
diff_instance .custom_report_result (
290
- _DIFF_ACTION_UPLOADED_FILE_CHANGED , level , {"value" : level .t2 }
320
+ _DIFF_ACTION_EQUALITY_CHANGED , level , {"value" : level .t2 }
291
321
)
292
322
return True
293
323
@@ -306,7 +336,7 @@ def diff_state(state1: Any, state2: Any) -> str:
306
336
raise MesopException ("Tried to diff state which was not a dataclass" )
307
337
308
338
custom_actions = []
309
- custom_operators = [UploadedFileOperator ()]
339
+ custom_operators = [EqualityOperator ()]
310
340
# Only use the `DataFrameOperator` if pandas exists.
311
341
if _has_pandas :
312
342
differences = DeepDiff (
@@ -328,15 +358,15 @@ def diff_state(state1: Any, state2: Any) -> str:
328
358
else :
329
359
differences = DeepDiff (state1 , state2 , custom_operators = custom_operators )
330
360
331
- # Manually format UploadedFile diffs to flat dict format.
332
- if _DIFF_ACTION_UPLOADED_FILE_CHANGED in differences :
361
+ # Manually format diffs to flat dict format.
362
+ if _DIFF_ACTION_EQUALITY_CHANGED in differences :
333
363
custom_actions = [
334
364
{
335
365
"path" : parse_path (path ),
336
- "action" : _DIFF_ACTION_UPLOADED_FILE_CHANGED ,
366
+ "action" : _DIFF_ACTION_EQUALITY_CHANGED ,
337
367
** diff ,
338
368
}
339
- for path , diff in differences [_DIFF_ACTION_UPLOADED_FILE_CHANGED ].items ()
369
+ for path , diff in differences [_DIFF_ACTION_EQUALITY_CHANGED ].items ()
340
370
]
341
371
342
372
# Handle the set case which will have a modified path after being JSON encoded.
0 commit comments