Ver Fonte

implement persistence using observable dicts

Falko Schindler há 1 ano atrás
pai
commit
81d1eb3a0e
6 ficheiros alterados com 18 adições e 79 exclusões
  1. 0 1
      nicegui/nicegui.py
  2. 11 50
      nicegui/storage.py
  3. 0 12
      poetry.lock
  4. 0 1
      pyproject.toml
  5. 1 2
      tests/conftest.py
  6. 6 13
      tests/test_storage.py

+ 0 - 1
nicegui/nicegui.py

@@ -80,7 +80,6 @@ def handle_startup(with_welcome_message: bool = True) -> None:
             safe_invoke(t)
     background_tasks.create(binding.loop())
     background_tasks.create(outbox.loop())
-    background_tasks.create(app.storage._loop())
     background_tasks.create(prune_clients())
     background_tasks.create(prune_slot_stacks())
     globals.state = globals.State.STARTED

+ 11 - 50
nicegui/storage.py

@@ -1,18 +1,15 @@
-import asyncio
 import contextvars
 import json
-import threading
 import uuid
 from collections.abc import MutableMapping
 from pathlib import Path
 from typing import Any, Dict, Iterator, Optional, Union
 
-import aiofiles
 from fastapi import Request
 from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
 from starlette.responses import Response
 
-from . import globals
+from . import globals, observables
 
 request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None)
 
@@ -39,44 +36,11 @@ class ReadOnlyDict(MutableMapping):
         return len(self._data)
 
 
-class PersistentDict(dict):
+class PersistentDict(observables.ObservableDict):
 
-    def __init__(self, filepath: Path, *args: Any, **kwargs: Any) -> None:
-        self.filepath = filepath
-        self.lock = threading.Lock()
-        self.load()
-        self.update(*args, **kwargs)
-        self.modified = bool(args or kwargs)
-
-    def clear(self) -> None:
-        with self.lock:
-            super().clear()
-            self.modified = True
-
-    def __setitem__(self, key: Any, value: Any) -> None:
-        with self.lock:
-            super().__setitem__(key, value)
-            self.modified = True
-
-    def __delitem__(self, key: Any) -> None:
-        with self.lock:
-            super().__delitem__(key)
-            self.modified = True
-
-    def load(self) -> None:
-        with self.lock:
-            if self.filepath.exists():
-                with open(self.filepath, 'r') as f:
-                    try:
-                        self.update(json.load(f))
-                    except json.JSONDecodeError:
-                        pass
-
-    async def backup(self) -> None:
-        data = dict(self)
-        if self.modified:
-            async with aiofiles.open(self.filepath, 'w') as f:
-                await f.write(json.dumps(data))
+    def __init__(self, filepath: Path) -> None:
+        data = json.loads(filepath.read_text()) if filepath.exists() else {}
+        super().__init__(data, lambda: filepath.write_text(json.dumps(self)))
 
 
 class RequestTrackingMiddleware(BaseHTTPMiddleware):
@@ -144,12 +108,9 @@ class Storage:
         """General storage shared between all users that is persisted on the server (where NiceGUI is executed)."""
         return self._general
 
-    async def backup(self) -> None:
-        await self._general.backup()
-        for user in self._users.values():
-            await user.backup()
-
-    async def _loop(self) -> None:
-        while True:
-            await self.backup()
-            await asyncio.sleep(1.0)
+    def clear(self) -> None:
+        """Clears all storage."""
+        self._general.clear()
+        self._users.clear()
+        for filepath in self.storage_dir.glob('storage_*.json'):
+            filepath.unlink()

+ 0 - 12
poetry.lock

@@ -1,17 +1,5 @@
 # This file is automatically @generated by Poetry and should not be changed by hand.
 
-[[package]]
-name = "aiofiles"
-version = "23.1.0"
-description = "File support for asyncio."
-category = "main"
-optional = false
-python-versions = ">=3.7,<4.0"
-files = [
-    {file = "aiofiles-23.1.0-py3-none-any.whl", hash = "sha256:9312414ae06472eb6f1d163f555e466a23aed1c8f60c30cccf7121dba2e53eb2"},
-    {file = "aiofiles-23.1.0.tar.gz", hash = "sha256:edd247df9a19e0db16534d4baaf536d6609a43e1de5401d7a4c1c148753a1635"},
-]
-
 [[package]]
 name = "anyio"
 version = "3.6.2"

+ 0 - 1
pyproject.toml

@@ -29,7 +29,6 @@ orjson = {version = "^3.8.6", markers = "platform_machine != 'i386' and platform
 pywebview = "^4.0.2"
 importlib_metadata = { version = "^6.0.0", markers = "python_version ~= '3.7'" } # Python 3.7 has no importlib.metadata
 itsdangerous = "^2.1.2"
-aiofiles = "^23.1.0"
 
 [tool.poetry.group.dev.dependencies]
 icecream = "^2.1.0"

+ 1 - 2
tests/conftest.py

@@ -44,8 +44,7 @@ def reset_globals() -> Generator[None, None, None]:
     # NOTE favicon routes must be removed separately because they are not "pages"
     [globals.app.routes.remove(r) for r in globals.app.routes if r.path.endswith('/favicon.ico')]
     importlib.reload(globals)
-    globals.app.storage.general.clear()
-    globals.app.storage._users.clear()
+    globals.app.storage.clear()
     globals.index_client = Client(page('/'), shared=True).__enter__()
     globals.app.get('/')(globals.index_client.build_response)
 

+ 6 - 13
tests/test_storage.py

@@ -1,4 +1,5 @@
 import asyncio
+from pathlib import Path
 
 from nicegui import Client, app, background_tasks, ui
 
@@ -84,25 +85,20 @@ async def test_access_user_storage_on_interaction(screen: Screen):
     screen.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.open('/')
     screen.click('switch')
-    screen.wait(1)
-    await app.storage.backup()
-    assert '{"test_switch": true}' in list(app.storage._users.values())[0].filepath.read_text()
+    screen.wait(0.5)
+    assert '{"test_switch": true}' in next(Path('.nicegui').glob('storage_user_*.json')).read_text()
 
 
 def test_access_user_storage_from_button_click_handler(screen: Screen):
     @ui.page('/')
     async def page():
-        async def inner():
-            app.storage.user['inner_function'] = 'works'
-            await app.storage.backup()
-
-        ui.button('test', on_click=inner)
+        ui.button('test', on_click=app.storage.user.update(inner_function='works'))
 
     screen.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.open('/')
     screen.click('test')
     screen.wait(1)
-    assert '{"inner_function": "works"}' in list(app.storage._users.values())[0].filepath.read_text()
+    assert '{"inner_function": "works"}' in next(Path('.nicegui').glob('storage_user_*.json')).read_text()
 
 
 async def test_access_user_storage_from_background_task(screen: Screen):
@@ -111,12 +107,11 @@ async def test_access_user_storage_from_background_task(screen: Screen):
         async def subtask():
             await asyncio.sleep(0.1)
             app.storage.user['subtask'] = 'works'
-            await app.storage.backup()
         background_tasks.create(subtask())
 
     screen.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.open('/')
-    assert '{"subtask": "works"}' in list(app.storage._users.values())[0].filepath.read_text()
+    assert '{"subtask": "works"}' in next(Path('.nicegui').glob('storage_user_*.json')).read_text()
 
 
 def test_user_and_general_storage_is_persisted(screen: Screen):
@@ -126,7 +121,6 @@ def test_user_and_general_storage_is_persisted(screen: Screen):
         app.storage.general['count'] = app.storage.general.get('count', 0) + 1
         ui.label(f'user: {app.storage.user["count"]}')
         ui.label(f'general: {app.storage.general["count"]}')
-        ui.button('backup', on_click=app.storage.backup)
 
     screen.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.open('/')
@@ -134,7 +128,6 @@ def test_user_and_general_storage_is_persisted(screen: Screen):
     screen.open('/')
     screen.should_contain('user: 3')
     screen.should_contain('general: 3')
-    screen.click('backup')
     screen.selenium.delete_all_cookies()
     screen.open('/')
     screen.should_contain('user: 1')