瀏覽代碼

Merge branch 'binding' into main

Falko Schindler 3 年之前
父節點
當前提交
196471ebb3
共有 13 個文件被更改,包括 145 次插入138 次删除
  1. 10 10
      examples.py
  2. 14 18
      main.py
  3. 60 0
      nicegui/binding.py
  4. 13 16
      nicegui/elements/button.py
  5. 8 7
      nicegui/elements/element.py
  6. 12 14
      nicegui/elements/image.py
  7. 12 14
      nicegui/elements/label.py
  8. 1 12
      nicegui/elements/svg.py
  9. 10 9
      nicegui/elements/value_element.py
  10. 2 7
      nicegui/nicegui.py
  11. 1 1
      nicegui/timer.py
  12. 2 29
      poetry.lock
  13. 0 1
      pyproject.toml

+ 10 - 10
examples.py

@@ -36,7 +36,7 @@ with ui.row():
                 ui.icon('far fa-clock')
                 clock = ui.label()
                 t = ui.timer(0.1, lambda: clock.set_text(datetime.now().strftime("%X")))
-            ui.checkbox('active').bind_value(t.active)
+            ui.checkbox('active').bind_value(t, 'active')
 
         with ui.card().classes('items-center'):
             ui.label('Style').classes('text-h5')
@@ -48,22 +48,22 @@ with ui.row():
         ui.label('Binding').classes('text-h5')
         with ui.row():
             n1 = ui.number(value=1.2345, format='%.2f')
-            n2 = ui.number(format='%.3f').bind_value(n1.value)
+            n2 = ui.number(format='%.3f').bind_value(n1, 'value')
         with ui.row():
             c = ui.checkbox('c1')
-            ui.switch('c2').bind_value(c.value)
-            ui.slider(min=0, max=1, value=0.5, step=0.01).bind_value_to(c.value, forward=lambda f: f > 0.5)
+            ui.switch('c2').bind_value(c, 'value')
+            ui.slider(min=0, max=1, value=0.5, step=0.01).bind_value_to(c, 'value', forward=lambda f: f > 0.5)
         with ui.row():
             model = type('Model', (), {'value': 1})  # one-liner to define an object with an attribute "value"
-            ui.radio({1: 'a', 2: 'b', 3: 'c'}).bind_value(model.value)
-            ui.radio({1: 'A', 2: 'B', 3: 'C'}).bind_value(model.value)
+            ui.radio({1: 'a', 2: 'b', 3: 'c'}).bind_value(model, 'value')
+            ui.radio({1: 'A', 2: 'B', 3: 'C'}).bind_value(model, 'value')
             with ui.column():
-                ui.number().bind_value(model.value)
-                ui.slider(min=1, max=3).bind_value(model.value)
-                ui.label().bind_text(model.value)
+                ui.number().bind_value(model, 'value')
+                ui.slider(min=1, max=3).bind_value(model, 'value')
+                ui.label().bind_text(model, 'value')
         with ui.row().classes('items-center'):
             v = ui.checkbox('visible', value=True)
-            ui.icon('visibility').bind_visibility_from(v.value)
+            ui.icon('visibility').bind_visibility_from(v, 'value')
 
     with ui.card():
         ui.label('Matplotlib').classes('text-h5')

+ 14 - 18
main.py

@@ -154,7 +154,7 @@ with example(ui.switch):
 
 with example(ui.slider):
     slider = ui.slider(min=0, max=100, value=50).props('label')
-    ui.label().bind_text_from(slider.value)
+    ui.label().bind_text_from(slider, 'value')
 
 with example(ui.input):
     ui.input(
@@ -168,20 +168,20 @@ with example(ui.number):
     number_input = ui.number(label='Number', value=3.1415927, format='%.2f')
     with ui.row():
         ui.label('underlying value: ')
-        ui.label().bind_text_from(number_input.value)
+        ui.label().bind_text_from(number_input, 'value')
 
 with example(ui.radio):
     radio = ui.radio([1, 2, 3], value=1).props('inline')
-    ui.radio({1: 'A', 2: 'B', 3: 'C'}, value=1).props('inline').bind_value(radio.value)
+    ui.radio({1: 'A', 2: 'B', 3: 'C'}, value=1).props('inline').bind_value(radio, 'value')
 
 with example(ui.toggle):
     toggle = ui.toggle([1, 2, 3], value=1)
-    ui.toggle({1: 'A', 2: 'B', 3: 'C'}, value=1).bind_value(toggle.value)
+    ui.toggle({1: 'A', 2: 'B', 3: 'C'}, value=1).bind_value(toggle, 'value')
 
 with example(ui.select):
     with ui.row():
         select = ui.select([1, 2, 3], value=1).props('inline')
-        ui.select({1: 'One', 2: 'Two', 3: 'Three'}, value=1).props('inline').bind_value(select.value)
+        ui.select({1: 'One', 2: 'Two', 3: 'Three'}, value=1).props('inline').bind_value(select, 'value')
 
 with example(ui.upload):
     ui.upload(on_upload=lambda files: content.set_text(files))
@@ -204,7 +204,7 @@ with example(ui.line_plot):
         [np.sin(datetime.now().timestamp()) + 0.02 * np.random.randn()],
         [np.cos(datetime.now().timestamp()) + 0.02 * np.random.randn()],
     ]), active=False)
-    ui.checkbox('active').bind_value(line_updates.active)
+    ui.checkbox('active').bind_value(line_updates, 'active')
 
 with example(ui.log):
     from datetime import datetime
@@ -305,16 +305,15 @@ Just pass a property of the model as parameter to these methods to create the bi
 '''
 with example(binding):
     class Demo:
-
         def __init__(self):
             self.number = 1
 
     demo = Demo()
     v = ui.checkbox('visible', value=True)
-    with ui.column().bind_visibility_from(v.value):
-        ui.slider(min=1, max=3).bind_value(demo.number)
-        ui.toggle({1: 'a', 2: 'b', 3: 'c'}).bind_value(demo.number)
-        ui.number().bind_value(demo.number)
+    with ui.column().bind_visibility_from(v, 'value'):
+        ui.slider(min=1, max=3).bind_value(demo, 'number')
+        ui.toggle({1: 'a', 2: 'b', 3: 'c'}).bind_value(demo, 'number')
+        ui.number().bind_value(demo, 'number')
 
 
 with example(ui.timer):
@@ -323,7 +322,7 @@ with example(ui.timer):
     with ui.row().classes('items-center'):
         clock = ui.label()
         t = ui.timer(interval=0.1, callback=lambda: clock.set_text(datetime.now().strftime("%X.%f")[:-5]))
-        ui.checkbox('active').bind_value(t.active)
+        ui.checkbox('active').bind_value(t, 'active')
 
     with ui.row():
         def lazy_update():
@@ -371,12 +370,9 @@ Routed paths must start with a `'/'`.
 with example(add_route):
     import starlette
 
-    ui.add_route(
-        starlette.routing.Route(
-            '/new/route',
-            lambda request: starlette.responses.PlainTextResponse('Response')
-        )
-    )
+    ui.add_route(starlette.routing.Route(
+        '/new/route', lambda _: starlette.responses.PlainTextResponse('Response')
+    ))
 
     ui.link('Try the new route!', '/new/route')
 

+ 60 - 0
nicegui/binding.py

@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+import asyncio
+from collections import defaultdict
+from justpy.htmlcomponents import HTMLBaseComponent
+
+bindings = defaultdict(list)
+bindable_properties = set()
+active_links = []
+
+async def loop():
+    while True:
+        visited = set()
+        invalidated_views = []
+        for source_obj, source_name, target_obj, target_name, transform in active_links:
+            value = transform(getattr(source_obj, source_name))
+            if getattr(target_obj, target_name) != value:
+                setattr(target_obj, target_name, value)
+                propagate(target_obj, target_name, visited)
+                if hasattr(target_obj, 'view') and isinstance(target_obj.view, HTMLBaseComponent):
+                    invalidated_views.append(target_obj.view)
+        for view in invalidated_views:
+            await view.update()
+        await asyncio.sleep(0.1)
+
+def propagate(source_obj, source_name, visited=None):
+    if visited is None:
+        visited = set()
+    visited.add((id(source_obj), source_name))
+    for _, target_obj, target_name, transform in bindings[(id(source_obj), source_name)]:
+        if (id(target_obj), target_name) in visited:
+            continue
+        target_value = transform(getattr(source_obj, source_name))
+        if getattr(target_obj, target_name) != target_value:
+            setattr(target_obj, target_name, target_value)
+            propagate(target_obj, target_name, visited)
+
+def bind_to(self_obj, self_name, other_obj, other_name, forward):
+    bindings[(id(self_obj), self_name)].append((self_obj, other_obj, other_name, forward))
+    if (id(self_obj), self_name) not in bindable_properties:
+        active_links.append((self_obj, self_name, other_obj, other_name, forward))
+    propagate(self_obj, self_name)
+
+def bind_from(self_obj, self_name, other_obj, other_name, backward):
+    bindings[(id(other_obj), other_name)].append((other_obj, self_obj, self_name, backward))
+    if (id(other_obj), other_name) not in bindable_properties:
+        active_links.append((other_obj, other_name, self_obj, self_name, backward))
+    propagate(other_obj, other_name)
+
+class BindableProperty:
+
+    def __set_name__(self, _, name):
+        self.name = name
+
+    def __get__(self, owner, _=None):
+        return getattr(owner, '_' + self.name)
+
+    def __set__(self, owner, value):
+        setattr(owner, '_' + self.name, value)
+        bindable_properties.add((id(owner), self.name))
+        propagate(owner, self.name)

+ 13 - 16
nicegui/elements/button.py

@@ -1,12 +1,13 @@
 import asyncio
 from typing import Awaitable, Callable, Union
-
 import justpy as jp
 
-from .element import Element
+from ..binding import bind_from, bind_to, BindableProperty
 from ..utils import handle_exceptions, provide_arguments, async_provide_arguments
+from .element import Element
 
 class Button(Element):
+    text = BindableProperty
 
     def __init__(self,
                  text: str = '',
@@ -22,31 +23,27 @@ class Button(Element):
         view = jp.QButton(label=text, color='primary')
         super().__init__(view)
 
+        self.text = text
+        self.bind_text_to(self.view, 'label')
+
         if on_click is not None:
             if asyncio.iscoroutinefunction(on_click):
                 view.on('click', handle_exceptions(async_provide_arguments(func=on_click, update_function=view.update)))
             else:
                 view.on('click', handle_exceptions(provide_arguments(on_click)))
 
-    @property
-    def text(self):
-        return self.view.label
-
-    @text.setter
-    def text(self, text: any):
-        self.view.label = text
-
     def set_text(self, text: str):
         self.text = text
 
-    def bind_text_to(self, target, forward=lambda x: x):
-        self.text.bind_to(target, forward=forward, nesting=1)
+    def bind_text_to(self, target_object, target_name, forward=lambda x: x):
+        bind_to(self, 'text', target_object, target_name, forward=forward)
         return self
 
-    def bind_text_from(self, target, backward=lambda x: x):
-        self.text.bind_from(target, backward=backward, nesting=1)
+    def bind_text_from(self, target_object, target_name, backward=lambda x: x):
+        bind_from(self, 'text', target_object, target_name, backward=backward)
         return self
 
-    def bind_text(self, target, forward=lambda x: x, backward=lambda x: x):
-        self.text.bind(target, forward=forward, backward=backward, nesting=1)
+    def bind_text(self, target_object, target_name, forward=lambda x: x, backward=lambda x: x):
+        bind_from(self, 'text', target_object, target_name, backward=backward)
+        bind_to(self, 'text', target_object, target_name, forward=forward)
         return self

+ 8 - 7
nicegui/elements/element.py

@@ -1,6 +1,6 @@
 import justpy as jp
 from enum import Enum
-from binding.binding import BindableProperty
+from ..binding import bind_from, bind_to, BindableProperty
 from ..globals import view_stack, page_stack
 
 class Element:
@@ -26,22 +26,23 @@ class Element:
         self.visible_ = visible
         (self.view.remove_class if self.visible_ else self.view.set_class)('hidden')
 
-    def bind_visibility_to(self, target, forward=lambda x: x):
-        self.visible.bind_to(target, forward=forward, nesting=1)
+    def bind_visibility_to(self, target_object, target_name, forward=lambda x: x):
+        bind_to(self, 'visible', target_object, target_name, forward=forward)
         return self
 
-    def bind_visibility_from(self, target, backward=lambda x: x, *, value=None):
+    def bind_visibility_from(self, target_object, target_name, backward=lambda x: x, *, value=None):
         if value is not None:
             def backward(x): return x == value
 
-        self.visible.bind_from(target, backward=backward, nesting=1)
+        bind_from(self, 'visible', target_object, target_name, backward=backward)
         return self
 
-    def bind_visibility(self, target, forward=lambda x: x, backward=None, *, value=None):
+    def bind_visibility(self, target_object, target_name, forward=lambda x: x, backward=None, *, value=None):
         if value is not None:
             def backward(x): return x == value
 
-        self.visible.bind(target, forward=forward, backward=backward, nesting=1)
+        bind_from(self, 'visible', target_object, target_name, backward=backward)
+        bind_to(self, 'visible', target_object, target_name, forward=forward)
         return self
 
     def classes(self, add: str = '', *, remove: str = '', replace: str = ''):

+ 12 - 14
nicegui/elements/image.py

@@ -1,7 +1,10 @@
 import justpy as jp
+
+from ..binding import bind_from, bind_to, BindableProperty
 from .group import Group
 
 class Image(Group):
+    source = BindableProperty
 
     def __init__(self,
                  source: str = '',
@@ -13,28 +16,23 @@ class Image(Group):
         :param source: the source of the image; can be an url or a base64 string
         """
         view = jp.QImg(src=source)
-
         super().__init__(view)
 
-    @property
-    def source(self):
-        return self.view.src
-
-    @source.setter
-    def source(self, source: any):
-        self.view.src = source
+        self.source = source
+        self.bind_source_to(self.view, 'src')
 
     def set_source(self, source: str):
         self.source = source
 
-    def bind_source_to(self, target, forward=lambda x: x):
-        self.source.bind_to(target, forward=forward, nesting=1)
+    def bind_source_to(self, target_object, target_name, forward=lambda x: x):
+        bind_to(self, 'source', target_object, target_name, forward=forward)
         return self
 
-    def bind_source_from(self, target, backward=lambda x: x):
-        self.source.bind_from(target, backward=backward, nesting=1)
+    def bind_source_from(self, target_object, target_name, backward=lambda x: x):
+        bind_from(self, 'source', target_object, target_name, backward=backward)
         return self
 
-    def bind_source(self, target, forward=lambda x: x, backward=lambda x: x):
-        self.source.bind(target, forward=forward, backward=backward, nesting=1)
+    def bind_source(self, target_object, target_name, forward=lambda x: x, backward=lambda x: x):
+        bind_from(self, 'source', target_object, target_name, backward=backward)
+        bind_to(self, 'source', target_object, target_name, forward=forward)
         return self

+ 12 - 14
nicegui/elements/label.py

@@ -1,7 +1,10 @@
 import justpy as jp
+
+from ..binding import bind_from, bind_to, BindableProperty
 from .element import Element
 
 class Label(Element):
+    text = BindableProperty
 
     def __init__(self,
                  text: str = '',
@@ -13,28 +16,23 @@ class Label(Element):
         :param text: the content of the label
         """
         view = jp.Div(text=text)
-
         super().__init__(view)
 
-    @property
-    def text(self):
-        return self.view.text
-
-    @text.setter
-    def text(self, text: any):
-        self.view.text = text
+        self.text = text
+        self.bind_text_to(self.view, 'text')
 
     def set_text(self, text: str):
         self.text = text
 
-    def bind_text_to(self, target, forward=lambda x: x):
-        self.text.bind_to(target, forward=forward, nesting=1)
+    def bind_text_to(self, target_object, target_name, forward=lambda x: x):
+        bind_to(self, 'text', target_object, target_name, forward=forward)
         return self
 
-    def bind_text_from(self, target, backward=lambda x: x):
-        self.text.bind_from(target, backward=backward, nesting=1)
+    def bind_text_from(self, target_object, target_name, backward=lambda x: x):
+        bind_from(self, 'text', target_object, target_name, backward=backward)
         return self
 
-    def bind_text(self, target, forward=lambda x: x, backward=lambda x: x):
-        self.text.bind(target, forward=forward, backward=backward, nesting=1)
+    def bind_text(self, target_object, target_name, forward=lambda x: x, backward=lambda x: x):
+        bind_from(self, 'text', target_object, target_name, backward=backward)
+        bind_to(self, 'text', target_object, target_name, forward=forward)
         return self

+ 1 - 12
nicegui/elements/svg.py

@@ -1,4 +1,5 @@
 import justpy as jp
+
 from .element import Element
 
 class Svg(Element):
@@ -27,15 +28,3 @@ class Svg(Element):
 
     def set_content(self, content: str):
         self.content = content
-
-    def bind_content_to(self, target, forward=lambda x: x):
-        self.content.bind_to(target, forward=forward, nesting=1)
-        return self
-
-    def bind_content_from(self, target, backward=lambda x: x):
-        self.content.bind_from(target, backward=backward, nesting=1)
-        return self
-
-    def bind_content(self, target, forward=lambda x: x, backward=lambda x: x):
-        self.content.bind(target, forward=forward, backward=backward, nesting=1)
-        return self

+ 10 - 9
nicegui/elements/value_element.py

@@ -1,9 +1,9 @@
 import justpy as jp
 from typing import Any, Callable
 import traceback
-from binding import BindableProperty
-from .element import Element
+from ..binding import bind_from, bind_to, BindableProperty
 from ..utils import EventArguments
+from .element import Element
 
 class ValueElement(Element):
     value = BindableProperty()
@@ -18,7 +18,7 @@ class ValueElement(Element):
 
         self.on_change = on_change
         self.value = value
-        self.value.bind_to(self.view.value, forward=self.value_to_view)
+        self.bind_value_to(self.view, 'value', forward=self.value_to_view)
 
     def value_to_view(self, value):
         return value
@@ -35,14 +35,15 @@ class ValueElement(Element):
             except Exception:
                 traceback.print_exc()
 
-    def bind_value_to(self, target, forward=lambda x: x):
-        self.value.bind_to(target, forward=forward, nesting=1)
+    def bind_value_to(self, target_object, target_name, *, forward=lambda x: x):
+        bind_to(self, 'value', target_object, target_name, forward=forward)
         return self
 
-    def bind_value_from(self, target, backward=lambda x: x):
-        self.value.bind_from(target, backward=backward, nesting=1)
+    def bind_value_from(self, target_object, target_name, *, backward=lambda x: x):
+        bind_from(self, 'value', target_object, target_name, backward=backward)
         return self
 
-    def bind_value(self, target, forward=lambda x: x, backward=lambda x: x):
-        self.value.bind(target, forward=forward, backward=backward, nesting=1)
+    def bind_value(self, target_object, target_name, *, forward=lambda x: x, backward=lambda x: x):
+        bind_from(self, 'value', target_object, target_name, backward=backward)
+        bind_to(self, 'value', target_object, target_name, forward=forward)
         return self

+ 2 - 7
nicegui/nicegui.py

@@ -1,19 +1,14 @@
 #!/usr/bin/env python3
 from typing import Awaitable, Callable
 import asyncio
-import binding
 
 from .ui import Ui  # NOTE: before justpy
 import justpy as jp
 from .timer import Timer
 from . import globals
+from . import binding
 
 
-async def binding_loop():
-    while True:
-        binding.update()
-        await asyncio.sleep(0.1)
-
 def create_task(coro):
     loop = asyncio.get_event_loop()
     return loop.create_task(coro)
@@ -25,7 +20,7 @@ def startup():
     tasks.extend(create_task(t) for t in Timer.tasks)
     tasks.extend(create_task(t) for t in Ui.startup_tasks if isinstance(t, Awaitable))
     [t() for t in Ui.startup_tasks if isinstance(t, Callable)]
-    jp.run_task(binding_loop())
+    jp.run_task(binding.loop())
 
 @jp.app.on_event('shutdown')
 def shutdown():

+ 1 - 1
nicegui/timer.py

@@ -2,7 +2,7 @@ import asyncio
 import time
 import traceback
 from typing import Awaitable, Callable, Union
-from binding import BindableProperty
+from .binding import BindableProperty
 from .globals import view_stack
 from .utils import handle_exceptions, handle_awaitable
 

+ 2 - 29
poetry.lock

@@ -72,18 +72,6 @@ python-versions = "*"
 pycodestyle = ">=2.7.0"
 toml = "*"
 
-[[package]]
-name = "binding"
-version = "0.3.1"
-description = "Bindable properties for Python"
-category = "main"
-optional = false
-python-versions = ">=3.7,<4.0"
-
-[package.dependencies]
-executing = ">=0.6.0,<0.7.0"
-forbiddenfruit = ">=0.1.4,<0.2.0"
-
 [[package]]
 name = "certifi"
 version = "2021.5.30"
@@ -151,15 +139,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 name = "executing"
 version = "0.6.0"
 description = "Get the currently executing AST node of a frame, and other information"
-category = "main"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "forbiddenfruit"
-version = "0.1.4"
-description = "Patch python built-in objects"
-category = "main"
+category = "dev"
 optional = false
 python-versions = "*"
 
@@ -499,7 +479,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "55fc3b0f5e6a7a4b2f7e6261ed736e30d3bee65d09bb7dd0a408d145365071de"
+content-hash = "53e5ec1f77169acd5b3d3ac737d34926a97510cc77899e428affae9e746a5f06"
 
 [metadata.files]
 addict = [
@@ -526,10 +506,6 @@ autopep8 = [
     {file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"},
     {file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"},
 ]
-binding = [
-    {file = "binding-0.3.1-py3-none-any.whl", hash = "sha256:1bd36c3c1abb388fb15bd6967d340a7025e23b54b622ca9a6e43d4c1caf14b55"},
-    {file = "binding-0.3.1.tar.gz", hash = "sha256:66bc5c465369ccc35c0d587865f1c9a9a8abc41cfa275f4adc748ae42dcc0d7f"},
-]
 certifi = [
     {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
     {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
@@ -615,9 +591,6 @@ executing = [
     {file = "executing-0.6.0-py2.py3-none-any.whl", hash = "sha256:a2f10f802b4312b92bd256279b43720271b0d9b540a0dbab7be4c28fbc536479"},
     {file = "executing-0.6.0.tar.gz", hash = "sha256:a07046e608c56948a899e1c7dc45327ed84ee67edf245041eb8c6722658c14e3"},
 ]
-forbiddenfruit = [
-    {file = "forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253"},
-]
 h11 = [
     {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
     {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},

+ 0 - 1
pyproject.toml

@@ -16,7 +16,6 @@ typing-extensions = "^3.10.0"
 markdown2 = "^2.4.0"
 Pygments = "^2.9.0"
 docutils = "^0.17.1"
-binding = "^0.3.1"
 asttokens = "^2.0.5"
 uvicorn = {extras = ["watchgodreloader"], version = "^0.14.0"}
 watchgod = "^0.7"