Bladeren bron

maint: change OutputReturn obj json serialization logic

wangweimin 5 jaren geleden
bovenliggende
commit
8c0f777565

+ 60 - 12
pywebio/io_ctrl.py

@@ -1,10 +1,10 @@
 """
 输入输出的底层实现函数
-
-
 """
+import inspect
 import json
 import logging
+from functools import partial, wraps
 
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
 
@@ -12,30 +12,78 @@ logger = logging.getLogger(__name__)
 
 
 class OutputReturn:
-    """ ``output`` 消息的处理类 """
+    """ ``put_xxx()`` 类函数的返回值
+
+    若 ``put_xxx()`` 调用的返回值没有被变量接收,则直接将消息发送到会话;
+    否则消息则作为其他消息的一部分
+    """
+
+    @staticmethod
+    def safely_destruct(obj):
+        """安全销毁 OutputReturn 对象, 使 OutputReturn.__del__ 不进行任何操作"""
+        try:
+            json.dumps(obj, default=partial(output_json_encoder, ignore_error=True))
+        except Exception:
+            pass
 
     def __init__(self, spec, on_embed=None):
-        self.spec = spec
         self.processed = False
         self.on_embed = on_embed or (lambda d: d)
+        try:
+            # todo 使用其他方式来转换spec
+            self.spec = json.loads(json.dumps(spec, default=output_json_encoder))  # this may raise TypeError
+        except TypeError:
+            self.processed = True  #
+            type(self).safely_destruct(spec)
+            raise
 
     def embed_data(self):
-        """返回供嵌入到布局中的数据,可以设置一些默认值"""
+        """返回供嵌入到其他消息中的数据,可以设置一些默认值"""
         self.processed = True
         return self.on_embed(self.spec)
 
     def __del__(self):
-        """未嵌入时的操作:直接输出消息"""
+        """返回值没有被变量接收时的操作:直接输出消息"""
         if not self.processed:
             send_msg('output', self.spec)
 
 
-class OutputEncoder(json.JSONEncoder):
-    def default(self, obj):
-        if isinstance(obj, OutputReturn):
-            return obj.embed_data()
-        # Let the base class default method raise the TypeError
-        return json.JSONEncoder.default(self, obj)
+def output_json_encoder(obj, ignore_error=False):
+    """json序列化与输出相关消息的Encoder函数 """
+    if isinstance(obj, OutputReturn):
+        return obj.embed_data()
+    if not ignore_error:
+        raise TypeError('Object of type  %s is not JSON serializable' % obj.__class__.__name__)
+
+
+def safely_destruct_output_when_exp(content_param):
+    """装饰器生成: 异常时安全释放 OutputReturn 对象
+
+    :param content_param: 含有OutputReturn实例的参数名或参数名列表
+    :type content_param: list/str
+    :return: 装饰器
+    """
+
+    def decorator(func):
+        sig = inspect.signature(func)
+
+        @wraps(func)
+        def inner(*args, **kwargs):
+            try:
+                return func(*args, **kwargs)
+            except Exception:
+                # 发生异常,安全地释放 OutputReturn 对象
+                params = [content_param] if isinstance(content_param, str) else content_param
+                bound = sig.bind(*args, **kwargs).arguments
+                for param in params:
+                    if bound.get(param):
+                        OutputReturn.safely_destruct(bound.get(param))
+
+                raise
+
+        return inner
+
+    return decorator
 
 
 def send_msg(cmd, spec=None):

+ 1 - 2
pywebio/platform/aiohttp.py

@@ -9,7 +9,6 @@ from urllib.parse import urlparse
 from aiohttp import web
 
 from .tornado import open_webbrowser_on_server_started
-from ..io_ctrl import OutputEncoder
 from ..session import CoroutineBasedSession, ThreadBasedSession, register_session_implement_for_target, AbstractSession
 from ..session.base import get_session_info_from_headers
 from ..utils import get_free_port, STATIC_PATH
@@ -59,7 +58,7 @@ def _webio_handler(target, session_cls, websocket_settings, check_origin_func=_i
 
         def send_msg_to_client(session: AbstractSession):
             for msg in session.get_task_commands():
-                msg_str = json.dumps(msg, cls=OutputEncoder)
+                msg_str = json.dumps(msg)
                 ioloop.create_task(ws.send_str(msg_str))
 
         def close_from_session():

+ 2 - 3
pywebio/platform/django.py

@@ -50,16 +50,15 @@ class DjangoHttpContext(HttpContext):
         """为当前响应设置http status"""
         self.response.status_code = status
 
-    def set_content(self, content, json_type=False, json_cls=None):
+    def set_content(self, content, json_type=False):
         """设置相应的内容
 
         :param content:
         :param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
-        :param json_cls: json.dumps 使用的JSONEncoder
         """
         if json_type:
             self.set_header('content-type', 'application/json')
-            self.response.content = json.dumps(content, cls=json_cls)
+            self.response.content = json.dumps(content)
         else:
             self.response.content = content
 

+ 2 - 3
pywebio/platform/flask.py

@@ -55,16 +55,15 @@ class FlaskHttpContext(HttpContext):
         """为当前响应设置http status"""
         self.response.status_code = status
 
-    def set_content(self, content, json_type=False, json_cls=None):
+    def set_content(self, content, json_type=False):
         """设置相应的内容
 
         :param content:
         :param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
-        :param json_cls: json.dumps 使用的JSONEncoder
         """
         if json_type:
             self.set_header('content-type', 'application/json')
-            self.response.data = json.dumps(content, cls=json_cls)
+            self.response.data = json.dumps(content)
         else:
             self.response.data = content
 

+ 2 - 5
pywebio/platform/httpbased.py

@@ -20,8 +20,6 @@ import threading
 from typing import Dict
 
 import time
-import json
-from ..io_ctrl import OutputEncoder
 from ..session import CoroutineBasedSession, AbstractSession, register_session_implement_for_target
 from ..session.base import get_session_info_from_headers
 from ..utils import random_str, LRUDict
@@ -60,12 +58,11 @@ class HttpContext:
         """为当前响应设置http status"""
         pass
 
-    def set_content(self, content, json_type=False, json_cls=None):
+    def set_content(self, content, json_type=False):
         """设置响应的内容。方法应该仅被调用一次
 
         :param content:
         :param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
-        :param json_cls: json.dumps 使用的JSONEncoder
         """
         pass
 
@@ -186,7 +183,7 @@ class HttpHandler:
             cls._last_check_session_expire_ts = time.time()
             self._remove_expired_sessions(self.session_expire_seconds)
 
-        context.set_content(webio_session.get_task_commands(), json_type=True, json_cls=OutputEncoder)
+        context.set_content(webio_session.get_task_commands(), json_type=True)
 
         if webio_session.closed():
             self._remove_webio_session(webio_session_id)

+ 1 - 2
pywebio/platform/tornado.py

@@ -14,7 +14,6 @@ import tornado.ioloop
 from tornado.web import StaticFileHandler
 from tornado.websocket import WebSocketHandler
 
-from ..io_ctrl import OutputEncoder
 from ..session import CoroutineBasedSession, ThreadBasedSession, ScriptModeSession, \
     register_session_implement_for_target, AbstractSession
 from ..session.base import get_session_info_from_headers
@@ -72,7 +71,7 @@ def _webio_handler(target, session_cls, check_origin_func=_is_same_site):
 
         def send_msg_to_client(self, session: AbstractSession):
             for msg in session.get_task_commands():
-                self.write_message(json.dumps(msg, cls=OutputEncoder))
+                self.write_message(json.dumps(msg))
 
         def open(self):
             logger.debug("WebSocket opened")