Quellcode durchsuchen

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

wangweimin vor 4 Jahren
Ursprung
Commit
863ef86d34
4 geänderte Dateien mit 182 neuen und 48 gelöschten Zeilen
  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:: defer_call
 .. 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:: set_env
 .. autofunction:: go_app
@@ -25,13 +97,13 @@ from .base import Session
 from .coroutinebased import CoroutineBasedSession
 from .threadbased import ThreadBasedSession, ScriptModeSession
 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 = []
 
 __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):
@@ -301,54 +373,21 @@ def defer_call(func):
     return func
 
 
+# session-local data object
+local = ObjectDictProxy(lambda: get_current_session().save)
+
 def data():
     """获取当前会话的数据对象(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):

+ 1 - 1
pywebio/session/base.py

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

+ 79 - 0
pywebio/utils.py

@@ -28,6 +28,85 @@ class Setter:
             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):
     """
     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
     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
     async def corofunc(**kwargs):
         pass
@@ -147,7 +164,6 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
     time.sleep(2)
 
 
-
 def start_test_server():
     pywebio.enable_debug()
     start_server({'coro': corobased, 'thread': threadbased}, port=8080, host='127.0.0.1', debug=True)