Ver Fonte

Merge pull request #978 from zauberzeug/observable-storage

Observable storage
Rodja Trappe há 2 anos atrás
pai
commit
c4aa52581b
4 ficheiros alterados com 25 adições e 62 exclusões
  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)
             safe_invoke(t)
     background_tasks.create(binding.loop())
     background_tasks.create(binding.loop())
     background_tasks.create(outbox.loop())
     background_tasks.create(outbox.loop())
-    background_tasks.create(app.storage._loop())
     background_tasks.create(prune_clients())
     background_tasks.create(prune_clients())
     background_tasks.create(prune_slot_stacks())
     background_tasks.create(prune_slot_stacks())
     globals.state = globals.State.STARTED
     globals.state = globals.State.STARTED

+ 18 - 46
nicegui/storage.py

@@ -1,7 +1,5 @@
-import asyncio
 import contextvars
 import contextvars
 import json
 import json
-import threading
 import uuid
 import uuid
 from collections.abc import MutableMapping
 from collections.abc import MutableMapping
 from pathlib import Path
 from pathlib import Path
@@ -12,7 +10,7 @@ from fastapi import Request
 from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
 from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
 from starlette.responses import Response
 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)
 request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None)
 
 
@@ -39,44 +37,21 @@ class ReadOnlyDict(MutableMapping):
         return len(self._data)
         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.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:
             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):
 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)."""
         """General storage shared between all users that is persisted on the server (where NiceGUI is executed)."""
         return self._general
         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"
     # 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')]
     [globals.app.routes.remove(r) for r in globals.app.routes if r.path.endswith('/favicon.ico')]
     importlib.reload(globals)
     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.index_client = Client(page('/'), shared=True).__enter__()
     globals.app.get('/')(globals.index_client.build_response)
     globals.app.get('/')(globals.index_client.build_response)
 
 

+ 6 - 13
tests/test_storage.py

@@ -1,4 +1,5 @@
 import asyncio
 import asyncio
+from pathlib import Path
 
 
 from nicegui import Client, app, background_tasks, ui
 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.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.open('/')
     screen.open('/')
     screen.click('switch')
     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):
 def test_access_user_storage_from_button_click_handler(screen: Screen):
     @ui.page('/')
     @ui.page('/')
     async def 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.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.open('/')
     screen.open('/')
     screen.click('test')
     screen.click('test')
     screen.wait(1)
     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):
 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():
         async def subtask():
             await asyncio.sleep(0.1)
             await asyncio.sleep(0.1)
             app.storage.user['subtask'] = 'works'
             app.storage.user['subtask'] = 'works'
-            await app.storage.backup()
         background_tasks.create(subtask())
         background_tasks.create(subtask())
 
 
     screen.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.open('/')
     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):
 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
         app.storage.general['count'] = app.storage.general.get('count', 0) + 1
         ui.label(f'user: {app.storage.user["count"]}')
         ui.label(f'user: {app.storage.user["count"]}')
         ui.label(f'general: {app.storage.general["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.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.open('/')
     screen.open('/')
@@ -134,7 +128,6 @@ def test_user_and_general_storage_is_persisted(screen: Screen):
     screen.open('/')
     screen.open('/')
     screen.should_contain('user: 3')
     screen.should_contain('user: 3')
     screen.should_contain('general: 3')
     screen.should_contain('general: 3')
-    screen.click('backup')
     screen.selenium.delete_all_cookies()
     screen.selenium.delete_all_cookies()
     screen.open('/')
     screen.open('/')
     screen.should_contain('user: 1')
     screen.should_contain('user: 1')