Przeglądaj źródła

Merge pull request #978 from zauberzeug/observable-storage

Observable storage
Rodja Trappe 2 lat temu
rodzic
commit
c4aa52581b
4 zmienionych plików z 25 dodań i 62 usunięć
  1. 0 1
      nicegui/nicegui.py
  2. 18 46
      nicegui/storage.py
  3. 1 2
      tests/conftest.py
  4. 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

+ 18 - 46
nicegui/storage.py

@@ -1,7 +1,5 @@
-import asyncio
 import contextvars
 import json
-import threading
 import uuid
 from collections.abc import MutableMapping
 from pathlib import Path
@@ -12,7 +10,7 @@ from fastapi import Request
 from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
 from starlette.responses import Response
 
-from . import globals
+from . import background_tasks, globals, observables
 
 request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None)
 
@@ -39,44 +37,21 @@ class ReadOnlyDict(MutableMapping):
         return len(self._data)
 
 
-class PersistentDict(dict):
+class PersistentDict(observables.ObservableDict):
 
-    def __init__(self, filepath: Path, *args: Any, **kwargs: Any) -> None:
+    def __init__(self, filepath: Path) -> None:
         self.filepath = filepath
-        self.lock = threading.Lock()
-        self.load()
-        self.update(*args, **kwargs)
-        self.modified = bool(args or kwargs)
+        data = json.loads(filepath.read_text()) if filepath.exists() else {}
+        super().__init__(data, self.backup)
 
-    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:
+    def backup(self) -> None:
+        async def backup() -> None:
             async with aiofiles.open(self.filepath, 'w') as f:
-                await f.write(json.dumps(data))
+                await f.write(json.dumps(self))
+        if globals.loop:
+            background_tasks.create_lazy(backup(), name=self.filepath.stem)
+        else:
+            globals.app.on_startup(backup())
 
 
 class RequestTrackingMiddleware(BaseHTTPMiddleware):
@@ -144,12 +119,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()

+ 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')