瀏覽代碼

Merge pull request #1537 from zauberzeug/app.urls

Get the list of urls on which the server is available
Falko Schindler 1 年之前
父節點
當前提交
ff39f3af84
共有 8 個文件被更改,包括 161 次插入103 次删除
  1. 1 0
      nicegui/air.py
  2. 2 0
      nicegui/app.py
  3. 2 2
      nicegui/events.py
  4. 117 89
      nicegui/observables.py
  5. 1 1
      nicegui/storage.py
  6. 5 4
      nicegui/welcome.py
  7. 18 7
      tests/test_observables.py
  8. 15 0
      website/documentation.py

+ 1 - 0
nicegui/air.py

@@ -55,6 +55,7 @@ class Air:
 
 
         @self.relay.on('ready')
         @self.relay.on('ready')
         def on_ready(data: Dict[str, Any]) -> None:
         def on_ready(data: Dict[str, Any]) -> None:
+            globals.app.urls.add(data['device_url'])
             print(f'NiceGUI is on air at {data["device_url"]}', flush=True)
             print(f'NiceGUI is on air at {data["device_url"]}', flush=True)
 
 
         @self.relay.on('error')
         @self.relay.on('error')

+ 2 - 0
nicegui/app.py

@@ -7,6 +7,7 @@ from fastapi.staticfiles import StaticFiles
 
 
 from . import globals, helpers  # pylint: disable=redefined-builtin
 from . import globals, helpers  # pylint: disable=redefined-builtin
 from .native import Native
 from .native import Native
+from .observables import ObservableSet
 from .storage import Storage
 from .storage import Storage
 
 
 
 
@@ -16,6 +17,7 @@ class App(FastAPI):
         super().__init__(**kwargs)
         super().__init__(**kwargs)
         self.native = Native()
         self.native = Native()
         self.storage = Storage()
         self.storage = Storage()
+        self.urls = ObservableSet()
 
 
     def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
     def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
         """Called every time a new client connects to NiceGUI.
         """Called every time a new client connects to NiceGUI.

+ 2 - 2
nicegui/events.py

@@ -12,7 +12,7 @@ from .slot import Slot
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from .client import Client
     from .client import Client
     from .element import Element
     from .element import Element
-    from .observables import ObservableDict, ObservableList, ObservableSet
+    from .observables import ObservableCollection
 
 
 
 
 @dataclass(**KWONLY_SLOTS)
 @dataclass(**KWONLY_SLOTS)
@@ -22,7 +22,7 @@ class EventArguments:
 
 
 @dataclass(**KWONLY_SLOTS)
 @dataclass(**KWONLY_SLOTS)
 class ObservableChangeEventArguments(EventArguments):
 class ObservableChangeEventArguments(EventArguments):
-    sender: Union[ObservableDict, ObservableList, ObservableSet]
+    sender: ObservableCollection
 
 
 
 
 @dataclass(**KWONLY_SLOTS)
 @dataclass(**KWONLY_SLOTS)

+ 117 - 89
nicegui/observables.py

@@ -1,212 +1,240 @@
-from typing import Any, Callable, Dict, Iterable, List, Set, SupportsIndex, Union, overload
+from __future__ import annotations
 
 
-from . import events
+import abc
+from typing import Any, Callable, Collection, Dict, Iterable, List, Optional, SupportsIndex, Union
 
 
+from . import events
 
 
-class ObservableDict(dict):
 
 
-    def __init__(self, data: Dict, on_change: Callable) -> None:
-        super().__init__(data)
+class ObservableCollection(abc.ABC):
+
+    def __init__(self, *,
+                 factory: Callable,
+                 data: Optional[Collection],
+                 on_change: Optional[Callable],
+                 _parent: Optional[ObservableCollection],
+                 ) -> None:
+        super().__init__(factory() if data is None else data)  # type: ignore
+        self._parent = _parent
+        self._change_handlers: List[Callable] = [on_change] if on_change else []
+
+    @property
+    def change_handlers(self) -> List[Callable]:
+        """Return a list of all change handlers registered on this collection and its parents."""
+        change_handlers = self._change_handlers[:]
+        if self._parent is not None:
+            change_handlers.extend(self._parent.change_handlers)
+        return change_handlers
+
+    def _handle_change(self) -> None:
+        for handler in self.change_handlers:
+            events.handle_event(handler, events.ObservableChangeEventArguments(sender=self))
+
+    def on_change(self, handler: Callable) -> None:
+        """Register a handler to be called when the collection changes."""
+        self._change_handlers.append(handler)
+
+    def _observe(self, data: Any) -> Any:
+        if isinstance(data, dict):
+            return ObservableDict(data, _parent=self)
+        if isinstance(data, list):
+            return ObservableList(data, _parent=self)
+        if isinstance(data, set):
+            return ObservableSet(data, _parent=self)
+        return data
+
+
+class ObservableDict(ObservableCollection, dict):
+
+    def __init__(self,
+                 data: Dict = None,  # type: ignore
+                 *,
+                 on_change: Optional[Callable] = None,
+                 _parent: Optional[ObservableCollection] = None,
+                 ) -> None:
+        super().__init__(factory=dict, data=data, on_change=on_change, _parent=_parent)
         for key, value in self.items():
         for key, value in self.items():
-            super().__setitem__(key, make_observable(value, on_change))
-        self.on_change = lambda: events.handle_event(on_change, events.ObservableChangeEventArguments(sender=self))
+            super().__setitem__(key, self._observe(value))
 
 
     def pop(self, k: Any, d: Any = None) -> Any:
     def pop(self, k: Any, d: Any = None) -> Any:
         item = super().pop(k, d)
         item = super().pop(k, d)
-        self.on_change()
+        self._handle_change()
         return item
         return item
 
 
     def popitem(self) -> Any:
     def popitem(self) -> Any:
         item = super().popitem()
         item = super().popitem()
-        self.on_change()
+        self._handle_change()
         return item
         return item
 
 
     def update(self, *args: Any, **kwargs: Any) -> None:
     def update(self, *args: Any, **kwargs: Any) -> None:
-        super().update(make_observable(dict(*args, **kwargs), self.on_change))
-        self.on_change()
+        super().update(self._observe(dict(*args, **kwargs)))
+        self._handle_change()
 
 
     def clear(self) -> None:
     def clear(self) -> None:
         super().clear()
         super().clear()
-        self.on_change()
+        self._handle_change()
 
 
     def setdefault(self, __key: Any, __default: Any = None) -> Any:
     def setdefault(self, __key: Any, __default: Any = None) -> Any:
-        item = super().setdefault(__key, make_observable(__default, self.on_change))
-        self.on_change()
+        item = super().setdefault(__key, self._observe(__default))
+        self._handle_change()
         return item
         return item
 
 
     def __setitem__(self, __key: Any, __value: Any) -> None:
     def __setitem__(self, __key: Any, __value: Any) -> None:
-        super().__setitem__(__key, make_observable(__value, self.on_change))
-        self.on_change()
+        super().__setitem__(__key, self._observe(__value))
+        self._handle_change()
 
 
     def __delitem__(self, __key: Any) -> None:
     def __delitem__(self, __key: Any) -> None:
         super().__delitem__(__key)
         super().__delitem__(__key)
-        self.on_change()
+        self._handle_change()
 
 
     def __or__(self, other: Any) -> Any:
     def __or__(self, other: Any) -> Any:
         return super().__or__(other)
         return super().__or__(other)
 
 
     def __ior__(self, other: Any) -> Any:
     def __ior__(self, other: Any) -> Any:
-        super().__ior__(make_observable(dict(other), self.on_change))
-        self.on_change()
+        super().__ior__(self._observe(dict(other)))
+        self._handle_change()
         return self
         return self
 
 
 
 
-class ObservableList(list):
+class ObservableList(ObservableCollection, list):
 
 
-    def __init__(self, data: List, on_change: Callable) -> None:
-        super().__init__(data)
+    def __init__(self,
+                 data: List = None,  # type: ignore
+                 *,
+                 on_change: Optional[Callable] = None,
+                 _parent: Optional[ObservableCollection] = None,
+                 ) -> None:
+        super().__init__(factory=list, data=data, on_change=on_change, _parent=_parent)
         for i, item in enumerate(self):
         for i, item in enumerate(self):
-            super().__setitem__(i, make_observable(item, on_change))
-        self.on_change = lambda: events.handle_event(on_change, events.ObservableChangeEventArguments(sender=self))
+            super().__setitem__(i, self._observe(item))
 
 
     def append(self, item: Any) -> None:
     def append(self, item: Any) -> None:
-        super().append(make_observable(item, self.on_change))
-        self.on_change()
+        super().append(self._observe(item))
+        self._handle_change()
 
 
     def extend(self, iterable: Iterable) -> None:
     def extend(self, iterable: Iterable) -> None:
-        super().extend(make_observable(list(iterable), self.on_change))
-        self.on_change()
+        super().extend(self._observe(list(iterable)))
+        self._handle_change()
 
 
     def insert(self, index: SupportsIndex, obj: Any) -> None:
     def insert(self, index: SupportsIndex, obj: Any) -> None:
-        super().insert(index, make_observable(obj, self.on_change))
-        self.on_change()
+        super().insert(index, self._observe(obj))
+        self._handle_change()
 
 
     def remove(self, value: Any) -> None:
     def remove(self, value: Any) -> None:
         super().remove(value)
         super().remove(value)
-        self.on_change()
+        self._handle_change()
 
 
     def pop(self, index: SupportsIndex = -1) -> Any:
     def pop(self, index: SupportsIndex = -1) -> Any:
         item = super().pop(index)
         item = super().pop(index)
-        self.on_change()
+        self._handle_change()
         return item
         return item
 
 
     def clear(self) -> None:
     def clear(self) -> None:
         super().clear()
         super().clear()
-        self.on_change()
+        self._handle_change()
 
 
     def sort(self, **kwargs: Any) -> None:
     def sort(self, **kwargs: Any) -> None:
         super().sort(**kwargs)
         super().sort(**kwargs)
-        self.on_change()
+        self._handle_change()
 
 
     def reverse(self) -> None:
     def reverse(self) -> None:
         super().reverse()
         super().reverse()
-        self.on_change()
+        self._handle_change()
 
 
     def __delitem__(self, key: Union[SupportsIndex, slice]) -> None:
     def __delitem__(self, key: Union[SupportsIndex, slice]) -> None:
         super().__delitem__(key)
         super().__delitem__(key)
-        self.on_change()
+        self._handle_change()
 
 
     def __setitem__(self, key: Union[SupportsIndex, slice], value: Any) -> None:
     def __setitem__(self, key: Union[SupportsIndex, slice], value: Any) -> None:
-        super().__setitem__(key, make_observable(value, self.on_change))
-        self.on_change()
+        super().__setitem__(key, self._observe(value))
+        self._handle_change()
 
 
     def __add__(self, other: Any) -> Any:
     def __add__(self, other: Any) -> Any:
         return super().__add__(other)
         return super().__add__(other)
 
 
     def __iadd__(self, other: Any) -> Any:
     def __iadd__(self, other: Any) -> Any:
-        super().__iadd__(make_observable(other, self.on_change))
-        self.on_change()
+        super().__iadd__(self._observe(other))
+        self._handle_change()
         return self
         return self
 
 
 
 
-class ObservableSet(set):
+class ObservableSet(ObservableCollection, set):
 
 
-    def __init__(self, data: set, on_change: Callable) -> None:
-        super().__init__(data)
+    def __init__(self,
+                 data: set = None,  # type: ignore
+                 *,
+                 on_change: Optional[Callable] = None,
+                 _parent: Optional[ObservableCollection] = None,
+                 ) -> None:
+        super().__init__(factory=set, data=data, on_change=on_change, _parent=_parent)
         for item in self:
         for item in self:
-            super().add(make_observable(item, on_change))
-        self.on_change = lambda: events.handle_event(on_change, events.ObservableChangeEventArguments(sender=self))
+            super().add(self._observe(item))
 
 
     def add(self, item: Any) -> None:
     def add(self, item: Any) -> None:
-        super().add(make_observable(item, self.on_change))
-        self.on_change()
+        super().add(self._observe(item))
+        self._handle_change()
 
 
     def remove(self, item: Any) -> None:
     def remove(self, item: Any) -> None:
         super().remove(item)
         super().remove(item)
-        self.on_change()
+        self._handle_change()
 
 
     def discard(self, item: Any) -> None:
     def discard(self, item: Any) -> None:
         super().discard(item)
         super().discard(item)
-        self.on_change()
+        self._handle_change()
 
 
     def pop(self) -> Any:
     def pop(self) -> Any:
         item = super().pop()
         item = super().pop()
-        self.on_change()
+        self._handle_change()
         return item
         return item
 
 
     def clear(self) -> None:
     def clear(self) -> None:
         super().clear()
         super().clear()
-        self.on_change()
+        self._handle_change()
 
 
     def update(self, *s: Iterable[Any]) -> None:
     def update(self, *s: Iterable[Any]) -> None:
-        super().update(make_observable(set(*s), self.on_change))
-        self.on_change()
+        super().update(self._observe(set(*s)))
+        self._handle_change()
 
 
     def intersection_update(self, *s: Iterable[Any]) -> None:
     def intersection_update(self, *s: Iterable[Any]) -> None:
         super().intersection_update(*s)
         super().intersection_update(*s)
-        self.on_change()
+        self._handle_change()
 
 
     def difference_update(self, *s: Iterable[Any]) -> None:
     def difference_update(self, *s: Iterable[Any]) -> None:
         super().difference_update(*s)
         super().difference_update(*s)
-        self.on_change()
+        self._handle_change()
 
 
     def symmetric_difference_update(self, *s: Iterable[Any]) -> None:
     def symmetric_difference_update(self, *s: Iterable[Any]) -> None:
         super().symmetric_difference_update(*s)
         super().symmetric_difference_update(*s)
-        self.on_change()
+        self._handle_change()
 
 
     def __or__(self, other: Any) -> Any:
     def __or__(self, other: Any) -> Any:
         return super().__or__(other)
         return super().__or__(other)
 
 
     def __ior__(self, other: Any) -> Any:
     def __ior__(self, other: Any) -> Any:
-        super().__ior__(make_observable(other, self.on_change))
-        self.on_change()
+        super().__ior__(self._observe(other))
+        self._handle_change()
         return self
         return self
 
 
     def __and__(self, other: Any) -> set:
     def __and__(self, other: Any) -> set:
         return super().__and__(other)
         return super().__and__(other)
 
 
     def __iand__(self, other: Any) -> Any:
     def __iand__(self, other: Any) -> Any:
-        super().__iand__(make_observable(other, self.on_change))
-        self.on_change()
+        super().__iand__(self._observe(other))
+        self._handle_change()
         return self
         return self
 
 
     def __sub__(self, other: Any) -> set:
     def __sub__(self, other: Any) -> set:
         return super().__sub__(other)
         return super().__sub__(other)
 
 
     def __isub__(self, other: Any) -> Any:
     def __isub__(self, other: Any) -> Any:
-        super().__isub__(make_observable(other, self.on_change))
-        self.on_change()
+        super().__isub__(self._observe(other))
+        self._handle_change()
         return self
         return self
 
 
     def __xor__(self, other: Any) -> set:
     def __xor__(self, other: Any) -> set:
         return super().__xor__(other)
         return super().__xor__(other)
 
 
     def __ixor__(self, other: Any) -> Any:
     def __ixor__(self, other: Any) -> Any:
-        super().__ixor__(make_observable(other, self.on_change))
-        self.on_change()
+        super().__ixor__(self._observe(other))
+        self._handle_change()
         return self
         return self
-
-
-@overload
-def make_observable(data: Dict, on_change: Callable) -> ObservableDict:
-    ...
-
-
-@overload
-def make_observable(data: List, on_change: Callable) -> ObservableList:
-    ...
-
-
-@overload
-def make_observable(data: Set, on_change: Callable) -> ObservableSet:
-    ...
-
-
-def make_observable(data: Any, on_change: Callable) -> Any:
-    if isinstance(data, dict):
-        return ObservableDict(data, on_change)
-    if isinstance(data, list):
-        return ObservableList(data, on_change)
-    if isinstance(data, set):
-        return ObservableSet(data, on_change)
-    return data

+ 1 - 1
nicegui/storage.py

@@ -42,7 +42,7 @@ class PersistentDict(observables.ObservableDict):
     def __init__(self, filepath: Path) -> None:
     def __init__(self, filepath: Path) -> None:
         self.filepath = filepath
         self.filepath = filepath
         data = json.loads(filepath.read_text()) if filepath.exists() else {}
         data = json.loads(filepath.read_text()) if filepath.exists() else {}
-        super().__init__(data, self.backup)
+        super().__init__(data, on_change=self.backup)
 
 
     def backup(self) -> None:
     def backup(self) -> None:
         if not self.filepath.exists():
         if not self.filepath.exists():

+ 5 - 4
nicegui/welcome.py

@@ -36,10 +36,11 @@ async def print_message() -> None:
     loop = asyncio.get_running_loop()
     loop = asyncio.get_running_loop()
     ips = set((await loop.run_in_executor(None, get_all_ips)) if host == '0.0.0.0' else [])
     ips = set((await loop.run_in_executor(None, get_all_ips)) if host == '0.0.0.0' else [])
     ips.discard('127.0.0.1')
     ips.discard('127.0.0.1')
-    addresses = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
-    if len(addresses) >= 2:
-        addresses[-1] = 'and ' + addresses[-1]
+    urls = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
+    globals.app.urls.update(urls)
+    if len(urls) >= 2:
+        urls[-1] = 'and ' + urls[-1]
     extra = ''
     extra = ''
     if 'netifaces' not in globals.optional_features:
     if 'netifaces' not in globals.optional_features:
         extra = ' (install netifaces to show all IPs and speedup this message)'
         extra = ' (install netifaces to show all IPs and speedup this message)'
-    print(f'on {", ".join(addresses)}' + extra, flush=True)
+    print(f'on {", ".join(urls)}' + extra, flush=True)

+ 18 - 7
tests/test_observables.py

@@ -2,7 +2,7 @@ import asyncio
 import sys
 import sys
 
 
 from nicegui import ui
 from nicegui import ui
-from nicegui.observables import make_observable
+from nicegui.observables import ObservableDict, ObservableList, ObservableSet
 
 
 from .screen import Screen
 from .screen import Screen
 
 
@@ -28,7 +28,7 @@ async def increment_counter_slowly(_):
 
 
 def test_observable_dict():
 def test_observable_dict():
     reset_counter()
     reset_counter()
-    data = make_observable({}, increment_counter)
+    data = ObservableDict(on_change=increment_counter)
     data['a'] = 1
     data['a'] = 1
     assert count == 1
     assert count == 1
     del data['a']
     del data['a']
@@ -50,7 +50,7 @@ def test_observable_dict():
 
 
 def test_observable_list():
 def test_observable_list():
     reset_counter()
     reset_counter()
-    data = make_observable([], increment_counter)
+    data = ObservableList(on_change=increment_counter)
     data.append(1)
     data.append(1)
     assert count == 1
     assert count == 1
     data.extend([2, 3, 4])
     data.extend([2, 3, 4])
@@ -81,7 +81,7 @@ def test_observable_list():
 
 
 def test_observable_set():
 def test_observable_set():
     reset_counter()
     reset_counter()
-    data = make_observable({1, 2, 3, 4, 5}, increment_counter)
+    data = ObservableSet({1, 2, 3, 4, 5}, on_change=increment_counter)
     data.add(1)
     data.add(1)
     assert count == 1
     assert count == 1
     data.remove(1)
     data.remove(1)
@@ -112,12 +112,12 @@ def test_observable_set():
 
 
 def test_nested_observables():
 def test_nested_observables():
     reset_counter()
     reset_counter()
-    data = make_observable({
+    data = ObservableDict({
         'a': 1,
         'a': 1,
         'b': [1, 2, 3, {'x': 1, 'y': 2, 'z': 3}],
         'b': [1, 2, 3, {'x': 1, 'y': 2, 'z': 3}],
         'c': {'x': 1, 'y': 2, 'z': 3, 't': [1, 2, 3]},
         'c': {'x': 1, 'y': 2, 'z': 3, 't': [1, 2, 3]},
         'd': {1, 2, 3},
         'd': {1, 2, 3},
-    }, increment_counter)
+    }, on_change=increment_counter)
     data['a'] = 42
     data['a'] = 42
     assert count == 1
     assert count == 1
     data['b'].append(4)
     data['b'].append(4)
@@ -134,7 +134,7 @@ def test_nested_observables():
 
 
 def test_async_handler(screen: Screen):
 def test_async_handler(screen: Screen):
     reset_counter()
     reset_counter()
-    data = make_observable([], increment_counter_slowly)
+    data = ObservableList(on_change=increment_counter_slowly)
     ui.button('Append 42', on_click=lambda: data.append(42))
     ui.button('Append 42', on_click=lambda: data.append(42))
 
 
     screen.open('/')
     screen.open('/')
@@ -143,3 +143,14 @@ def test_async_handler(screen: Screen):
     screen.click('Append 42')
     screen.click('Append 42')
     screen.wait(0.5)
     screen.wait(0.5)
     assert count == 1
     assert count == 1
+
+
+def test_setting_change_handler():
+    reset_counter()
+    data = ObservableList()
+    data.append(1)
+    assert count == 0
+
+    data.on_change(increment_counter)
+    data.append(2)
+    assert count == 1

+ 15 - 0
website/documentation.py

@@ -531,6 +531,21 @@ def create_full() -> None:
         ui.button('shutdown', on_click=lambda: ui.notify(
         ui.button('shutdown', on_click=lambda: ui.notify(
             'Nah. We do not actually shutdown the documentation server. Try it in your own app!'))
             'Nah. We do not actually shutdown the documentation server. Try it in your own app!'))
 
 
+    @text_demo('URLs', '''
+        You can access the list of all URLs on which the NiceGUI app is available via `app.urls`.
+        The URLs are not available in `app.on_startup` because the server is not yet running.
+        Instead, you can access them in a page function or register a callback with `app.urls.on_change`.
+    ''')
+    def urls_demo():
+        from nicegui import app
+
+        # @ui.page('/')
+        # def index():
+        #     for url in app.urls:
+        #         ui.link(url, target=url)
+        # END OF DEMO
+        ui.link('https://nicegui.io', target='https://nicegui.io')
+
     heading('NiceGUI Fundamentals')
     heading('NiceGUI Fundamentals')
 
 
     @text_demo('Auto-context', '''
     @text_demo('Auto-context', '''