diff --git a/eel/__init__.py b/eel/__init__.py index fd09b45b..db4a1adf 100644 --- a/eel/__init__.py +++ b/eel/__init__.py @@ -1,7 +1,7 @@ from builtins import range import traceback from io import open -from typing import Union, Any, Dict, List, Set, Tuple, Optional, Callable, TYPE_CHECKING +from typing import Union, Any, Dict, List, Set, Tuple, Optional, Callable, TYPE_CHECKING, cast, Type if TYPE_CHECKING: from eel.types import OptionsDictT, WebSocketT @@ -28,6 +28,7 @@ mimetypes.add_type('application/javascript', '.js') _eel_js_file: str = pkg.resource_filename('eel', 'eel.js') _eel_js: str = open(_eel_js_file, encoding='utf-8').read() +_eel_json_dumps_default_function: Callable[[Any], Any] = lambda o: None _websockets: List[Tuple[Any, WebSocketT]] = [] _call_return_values: Dict[Any, Any] = {} _call_return_callbacks: Dict[float, Tuple[Callable[..., Any], Optional[Callable[..., Any]]]] = {} @@ -55,6 +56,8 @@ 'position': None, # (left, top) of main window 'geometry': {}, # Dictionary of size/position for all windows 'close_callback': None, # Callback for when all windows have closed + 'json_encoder': None, # Custom JSONEncoder to customize json data dumping + 'json_decoder': None, # Custom JSONDecoder to customize json data loading 'app_mode': True, # (Chrome specific option) 'all_interfaces': False, # Allow bottle server to listen for connections on all interfaces 'disable_cache': True, # Sets the no-store response header when serving assets @@ -223,7 +226,7 @@ def _eel() -> str: page = _eel_js.replace('/** _py_functions **/', '_py_functions: %s,' % list(_exposed_functions.keys())) page = page.replace('/** _start_geometry **/', - '_start_geometry: %s,' % _safe_json(start_geometry)) + '_start_geometry: %s,' % _safe_json_dumps(start_geometry)) btl.response.content_type = 'application/javascript' _set_response_headers(btl.response) return page @@ -259,7 +262,7 @@ def _websocket(ws: WebSocketT) -> None: page = btl.request.query.page if page not in _mock_queue_done: for call in _mock_queue: - _repeated_send(ws, _safe_json(call)) + _repeated_send(ws, _safe_json_dumps(call)) _mock_queue_done.add(page) _websockets += [(page, ws)] @@ -267,7 +270,7 @@ def _websocket(ws: WebSocketT) -> None: while True: msg = ws.receive() if msg is not None: - message = jsn.loads(msg) + message = _safe_json_loads(msg) spawn(_process_message, message, ws) else: _websockets.remove((page, ws)) @@ -298,8 +301,12 @@ def register_eel_routes(app: btl.Bottle) -> None: # Private functions -def _safe_json(obj: Any) -> str: - return jsn.dumps(obj, default=lambda o: None) +def _safe_json_loads(obj: str) -> Any: + return jsn.loads(obj, cls=cast(Optional[Type[jsn.JSONDecoder]], _start_args['json_decoder'])) + +def _safe_json_dumps(obj: Any) -> str: + return jsn.dumps(obj, cls=cast(Optional[Type[jsn.JSONEncoder]], _start_args['json_encoder']), + default=_eel_json_dumps_default_function if not _start_args['json_encoder'] else None) def _repeated_send(ws: WebSocketT, msg: str) -> None: @@ -324,7 +331,7 @@ def _process_message(message: Dict[str, Any], ws: WebSocketT) -> None: status = 'error' error_info['errorText'] = repr(e) error_info['errorTraceback'] = err_traceback - _repeated_send(ws, _safe_json({ 'return': message['call'], + _repeated_send(ws, _safe_json_dumps({ 'return': message['call'], 'status': status, 'value': return_val, 'error': error_info,})) @@ -375,7 +382,7 @@ def _mock_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], def _js_call(name: str, args: Any) -> Callable[[Optional[Callable[..., Any]], Optional[Callable[..., Any]]], Any]: call_object = _call_object(name, args) for _, ws in _websockets: - _repeated_send(ws, _safe_json(call_object)) + _repeated_send(ws, _safe_json_dumps(call_object)) return _call_return(call_object) @@ -409,7 +416,7 @@ def _detect_shutdown() -> None: def _websocket_close(page: str) -> None: global _shutdown - close_callback = _start_args.get('close_callback') + close_callback = cast(Callable[..., Any], _start_args.get('close_callback')) if close_callback is not None: if not callable(close_callback): diff --git a/eel/types.py b/eel/types.py index 55475816..b81ba10b 100644 --- a/eel/types.py +++ b/eel/types.py @@ -1,4 +1,4 @@ -from typing import Union, Dict, List, Tuple, Callable, Optional, Any, TYPE_CHECKING +from typing import Union, Dict, List, Tuple, Callable, Optional, Any, TYPE_CHECKING, Type # This business is slightly awkward, but needed for backward compatibility, # because Python < 3.7 doesn't have __future__/annotations, and <3.10 doesn't @@ -12,9 +12,12 @@ JinjaEnvironmentT = Environment # type: ignore from geventwebsocket.websocket import WebSocket WebSocketT = WebSocket + from json import JSONDecoder, JSONEncoder else: JinjaEnvironmentT = None WebSocketT = Any + JSONEncoder = Any + JSONDecoder = Any OptionsDictT = Dict[ str, @@ -22,7 +25,7 @@ Union[ str, bool, int, float, List[str], Tuple[int, int], Dict[str, Tuple[int, int]], - Callable[..., Any], JinjaEnvironmentT + Callable[..., Any], JinjaEnvironmentT, Type[JSONEncoder], Type[JSONDecoder] ] ] ] diff --git a/examples/10 - custom_json_encoder_decoder/.gitignore b/examples/10 - custom_json_encoder_decoder/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/examples/10 - custom_json_encoder_decoder/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/10 - custom_json_encoder_decoder/custom_json_encoder_decoder.py b/examples/10 - custom_json_encoder_decoder/custom_json_encoder_decoder.py new file mode 100644 index 00000000..b3df20ba --- /dev/null +++ b/examples/10 - custom_json_encoder_decoder/custom_json_encoder_decoder.py @@ -0,0 +1,48 @@ +import eel +import json +import datetime + +eel.init('web') + +@eel.expose +def py_json_data_sender(): + return { + 'datetime': datetime.datetime.now() + } + +@eel.expose +def py_json_data_loader(): + return eel.js_json_data_sender()(print_value) + +def print_value(v): + print('Got this value from javascript:') + print(v) + +# Custom Json Encoder. +class EelJsonEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, datetime.date): + return o.__str__() + + +# Custom Json Decoder. +class CustomDatetimeObj(object): + def __init__(self, datetime): + self.datetime = datetime + + def __str__(self): + return 'CustomDatetimeObj: %s' % self.datetime.__str__() + +def decoder_object_hook(o): + if 'datetime' in o: + return CustomDatetimeObj(datetime=o['datetime']) + else: + return o + +class EelJsonDecoder(json.JSONDecoder): + def __init__(self, object_hook=decoder_object_hook, *args, **kwargs): + super().__init__(object_hook=object_hook, *args, **kwargs) + + +eel.start('custom_json_encoder_decoder.html', json_encoder=EelJsonEncoder, + json_decoder=EelJsonDecoder, size=(400, 300)) diff --git a/examples/10 - custom_json_encoder_decoder/web/custom_json_encoder_decoder.html b/examples/10 - custom_json_encoder_decoder/web/custom_json_encoder_decoder.html new file mode 100644 index 00000000..ad45ad5a --- /dev/null +++ b/examples/10 - custom_json_encoder_decoder/web/custom_json_encoder_decoder.html @@ -0,0 +1,14 @@ + + +
+