浏览代码

maint: use `session.local` instead of `session.data()`

wangweimin 4 年之前
父节点
当前提交
863ef86d34
共有 4 个文件被更改,包括 182 次插入48 次删除
  1. 85 46
      pywebio/session/__init__.py
  2. 1 1
      pywebio/session/base.py
  3. 79 0
      pywebio/utils.py
  4. 17 1
      test/13.misc.py

+ 85 - 46
pywebio/session/__init__.py

@@ -8,6 +8,78 @@ r"""
 .. autofunction:: register_thread
 .. autofunction:: register_thread
 .. autofunction:: defer_call
 .. autofunction:: defer_call
 .. autofunction:: hold
 .. autofunction:: hold
+
+.. data:: pywebio.session.local
+
+    当前会话的数据对象(session-local object)。
+
+    ``local`` 是一个可以通过属性访问的字典,访问不存在的属性时会返回 ``None`` 而不是抛出异常。
+    ``local`` 不支持字典的方法,支持使用 ``in`` 操作符来判断键是否存在,可以使用 ``local._dict`` 获取底层的字典表示。
+
+    :使用场景:
+
+    当需要在多个函数中保存一些会话独立的数据时,使用session-local对象保存状态会比通过函数参数传递更方便。
+    以下是一个会话独立的计数器的实现示例::
+
+        from pywebio.session import local
+        def add():
+            local.cnt = (local.cnt or 0) + 1
+
+        def show():
+            put_text(local.cnt or 0)
+
+        def main():  # 会话独立的计数器
+            put_buttons(['Add counter', 'Show counter'], [add, show])
+            hold()
+
+    而通过函数参数传递状态的实现方式为::
+
+        from functools import partial
+        def add(cnt):
+            cnt[0] += 1
+
+        def show(cnt):
+            put_text(cnt[0])
+
+        def main():  # 会话独立的计数器
+            cnt = [0]  # 将计数器保存在数组中才可以实现引用传参
+            put_buttons(['Add counter', 'Show counter'], [partial(add, cnt), partial(show, cnt)])
+            hold()
+
+    当然,还可以通过函数闭包来实现相同的功能::
+
+        def main():  # 会话独立的计数器
+            cnt = 0
+
+            def add():
+                nonlocal cnt
+                cnt += 1
+
+            def show():
+                put_text(cnt)
+
+            put_buttons(['Add counter', 'Show counter'], [add, show])
+            hold()
+
+    :local 支持的操作:
+
+    ::
+
+        local.name = "Wang"
+        local.age = 22
+        assert local.foo is None
+        local[10] = "10"
+
+        for key in local:
+            print(key)
+
+        assert 'bar' not in local
+        assert 'name' in local
+
+        print(local._dict)
+
+    .. versionadded:: 1.1
+
 .. autofunction:: data
 .. autofunction:: data
 .. autofunction:: set_env
 .. autofunction:: set_env
 .. autofunction:: go_app
 .. autofunction:: go_app
@@ -25,13 +97,13 @@ from .base import Session
 from .coroutinebased import CoroutineBasedSession
 from .coroutinebased import CoroutineBasedSession
 from .threadbased import ThreadBasedSession, ScriptModeSession
 from .threadbased import ThreadBasedSession, ScriptModeSession
 from ..exceptions import SessionNotFoundException, SessionException
 from ..exceptions import SessionNotFoundException, SessionException
-from ..utils import iscoroutinefunction, isgeneratorfunction, run_as_function, to_coroutine
+from ..utils import iscoroutinefunction, isgeneratorfunction, run_as_function, to_coroutine, ObjectDictProxy
 
 
 # 当前进程中正在使用的会话实现的列表
 # 当前进程中正在使用的会话实现的列表
 _active_session_cls = []
 _active_session_cls = []
 
 
 __all__ = ['run_async', 'run_asyncio_coroutine', 'register_thread', 'hold', 'defer_call', 'data', 'get_info',
 __all__ = ['run_async', 'run_asyncio_coroutine', 'register_thread', 'hold', 'defer_call', 'data', 'get_info',
-           'run_js', 'eval_js', 'download', 'set_env', 'go_app']
+           'run_js', 'eval_js', 'download', 'set_env', 'go_app', 'local']
 
 
 
 
 def register_session_implement_for_target(target_func):
 def register_session_implement_for_target(target_func):
@@ -301,54 +373,21 @@ def defer_call(func):
     return func
     return func
 
 
 
 
+# session-local data object
+local = ObjectDictProxy(lambda: get_current_session().save)
+
 def data():
 def data():
     """获取当前会话的数据对象(session-local object)。
     """获取当前会话的数据对象(session-local object)。
 
 
-    访问数据对象不存在的属性时会返回None而不是抛出异常。
-
-    当需要在多个函数中保存一些会话独立的数据时,使用session-local对象保存状态会比通过函数参数传递更方便。
-    以下是一个会话独立的计数器的实现示例::
-
-        def add():
-            data().cnt = (data().cnt or 0) + 1
-
-        def show():
-            put_text(data().cnt or 0)
-
-        def main():  # 会话独立的计数器
-            put_buttons(['Add counter', 'Show counter'], [add, show])
-            hold()
-
-    而通过函数参数传递状态的实现方式为::
-
-        from functools import partial
-        def add(cnt):
-            cnt[0] += 1
-
-        def show(cnt):
-            put_text(cnt[0])
-
-        def main():  # 会话独立的计数器
-            cnt = [0]  # 将计数器保存在数组中才可以实现引用传参
-            put_buttons(['Add counter', 'Show counter'], [partial(add, cnt), partial(show, cnt)])
-            hold()
-
-    当然,还可以通过函数闭包来实现相同的功能::
-
-        def main():  # 会话独立的计数器
-            cnt = 0
-
-            def add():
-                nonlocal cnt
-                cnt += 1
-
-            def show():
-                put_text(cnt)
-
-            put_buttons(['Add counter', 'Show counter'], [add, show])
-            hold()
+    .. deprecated:: 1.1
+        Use `local <pywebio.session.local>` instead.
     """
     """
-    return get_current_session().save
+    global local
+
+    import warnings
+    warnings.warn("Passing 'dict' as keyword argument is deprecated",
+                  DeprecationWarning, stacklevel=2)
+    return local
 
 
 
 
 def set_env(**env_info):
 def set_env(**env_info):

+ 1 - 1
pywebio/session/base.py

@@ -58,7 +58,7 @@ class Session:
         :param session_info: 会话信息。可以通过 Session.info 访问
         :param session_info: 会话信息。可以通过 Session.info 访问
         """
         """
         self.info = session_info
         self.info = session_info
-        self.save = Setter()
+        self.save = {}
         self.scope_stack = defaultdict(lambda: ['ROOT'])  # task_id -> scope栈
         self.scope_stack = defaultdict(lambda: ['ROOT'])  # task_id -> scope栈
 
 
         self.deferred_functions = []  # 会话结束时运行的函数
         self.deferred_functions = []  # 会话结束时运行的函数

+ 79 - 0
pywebio/utils.py

@@ -28,6 +28,85 @@ class Setter:
             return None
             return None
 
 
 
 
+class ObjectDictProxy:
+    """
+    通过属性访问的字典。实例不维护底层字典,而是每次在访问时使用回调函数获取
+
+    在对象属性上保存的数据会被保存到底层字典中
+    访问数据对象不存在的属性时会返回None而不是抛出异常。
+    不能保存下划线开始的属性
+    用 ``obj._dict`` 获取对象的字典表示
+
+    Example::
+
+        d = {}
+        data = LazyObjectDict(lambda: d)
+
+        data.name = "Wang"
+        data.age = 22
+        assert data.foo is None
+        data[10] = "10"
+
+        for key in data:
+            print(key)
+
+        assert 'bar' not in data
+        assert 'name' in data
+
+        assert data._dict is d
+        print(data._dict)
+    """
+
+    def __init__(self, dict_getter):
+        # 使用 self.__dict__ 避免触发 __setattr__
+        self.__dict__['_dict_getter'] = dict_getter
+
+    @property
+    def _dict(self):
+        return self._dict_getter()
+
+    def __len__(self):
+        return len(self._dict)
+
+    def __getitem__(self, key):
+        if key in self._dict:
+            return self._dict[key]
+        raise KeyError(key)
+
+    def __setitem__(self, key, item):
+        self._dict[key] = item
+
+    def __delitem__(self, key):
+        del self._dict[key]
+
+    def __iter__(self):
+        return iter(self._dict)
+
+    def __contains__(self, key):
+        return key in self._dict
+
+    def __repr__(self):
+        return repr(self._dict)
+
+    def __setattr__(self, key, value):
+        """
+        无论属性是否存在都会被调用
+        使用 self.__dict__[name] = value  避免递归
+        """
+        assert not key.startswith('_'), "Cannot set attributes starting with underscore"
+        self._dict.__setitem__(key, value)
+
+    def __getattr__(self, item):
+        """访问一个不存在的属性时触发"""
+        return self._dict.get(item, None)
+
+    def __delattr__(self, item):
+        try:
+            del self._dict[item]
+        except KeyError:
+            pass
+
+
 class ObjectDict(dict):
 class ObjectDict(dict):
     """
     """
     Object like dict, every dict[key] can visite by dict.key
     Object like dict, every dict[key] can visite by dict.key

+ 17 - 1
test/13.misc.py

@@ -23,6 +23,23 @@ def target():
     g.one += 1
     g.one += 1
     assert g.one == 2
     assert g.one == 2
 
 
+    local.name = "Wang"
+    local.age = 22
+    assert len(local) == 3
+    assert local['age'] is local.age
+    assert local.foo is None
+    local[10] = "10"
+    del local['name']
+    del local.one
+
+    for key in local:
+        print(key)
+
+    assert 'bar' not in local
+    assert 'age' in local
+    assert local._dict == {'age': 22, 10: '10'}
+    print(local)
+
     # test pywebio.utils
     # test pywebio.utils
     async def corofunc(**kwargs):
     async def corofunc(**kwargs):
         pass
         pass
@@ -147,7 +164,6 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
     time.sleep(2)
     time.sleep(2)
 
 
 
 
-
 def start_test_server():
 def start_test_server():
     pywebio.enable_debug()
     pywebio.enable_debug()
     start_server({'coro': corobased, 'thread': threadbased}, port=8080, host='127.0.0.1', debug=True)
     start_server({'coro': corobased, 'thread': threadbased}, port=8080, host='127.0.0.1', debug=True)