Преглед изворни кода

new binding implementation (no code inspection, no cursing)

Falko Schindler пре 3 година
родитељ
комит
95127e72d7

+ 10 - 10
examples.py

@@ -36,7 +36,7 @@ with ui.row():
                 ui.icon('far fa-clock')
                 ui.icon('far fa-clock')
                 clock = ui.label()
                 clock = ui.label()
                 t = ui.timer(0.1, lambda: clock.set_text(datetime.now().strftime("%X")))
                 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'):
         with ui.card().classes('items-center'):
             ui.label('Style').classes('text-h5')
             ui.label('Style').classes('text-h5')
@@ -48,22 +48,22 @@ with ui.row():
         ui.label('Binding').classes('text-h5')
         ui.label('Binding').classes('text-h5')
         with ui.row():
         with ui.row():
             n1 = ui.number(value=1.2345, format='%.2f')
             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():
         with ui.row():
             c = ui.checkbox('c1')
             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():
         with ui.row():
             model = type('Model', (), {'value': 1})  # one-liner to define an object with an attribute "value"
             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():
             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'):
         with ui.row().classes('items-center'):
             v = ui.checkbox('visible', value=True)
             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():
     with ui.card():
         ui.label('Matplotlib').classes('text-h5')
         ui.label('Matplotlib').classes('text-h5')

+ 14 - 18
main.py

@@ -154,7 +154,7 @@ with example(ui.switch):
 
 
 with example(ui.slider):
 with example(ui.slider):
     slider = ui.slider(min=0, max=100, value=50).props('label')
     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):
 with example(ui.input):
     ui.input(
     ui.input(
@@ -168,20 +168,20 @@ with example(ui.number):
     number_input = ui.number(label='Number', value=3.1415927, format='%.2f')
     number_input = ui.number(label='Number', value=3.1415927, format='%.2f')
     with ui.row():
     with ui.row():
         ui.label('underlying value: ')
         ui.label('underlying value: ')
-        ui.label().bind_text_from(number_input.value)
+        ui.label().bind_text_from(number_input, 'value')
 
 
 with example(ui.radio):
 with example(ui.radio):
     radio = ui.radio([1, 2, 3], value=1).props('inline')
     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):
 with example(ui.toggle):
     toggle = ui.toggle([1, 2, 3], value=1)
     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 example(ui.select):
     with ui.row():
     with ui.row():
         select = ui.select([1, 2, 3], value=1).props('inline')
         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):
 with example(ui.upload):
     ui.upload(on_upload=lambda files: content.set_text(files))
     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.sin(datetime.now().timestamp()) + 0.02 * np.random.randn()],
         [np.cos(datetime.now().timestamp()) + 0.02 * np.random.randn()],
         [np.cos(datetime.now().timestamp()) + 0.02 * np.random.randn()],
     ]), active=False)
     ]), active=False)
-    ui.checkbox('active').bind_value(line_updates.active)
+    ui.checkbox('active').bind_value(line_updates, 'active')
 
 
 with example(ui.log):
 with example(ui.log):
     from datetime import datetime
     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):
 with example(binding):
     class Demo:
     class Demo:
-
         def __init__(self):
         def __init__(self):
             self.number = 1
             self.number = 1
 
 
     demo = Demo()
     demo = Demo()
     v = ui.checkbox('visible', value=True)
     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):
 with example(ui.timer):
@@ -323,7 +322,7 @@ with example(ui.timer):
     with ui.row().classes('items-center'):
     with ui.row().classes('items-center'):
         clock = ui.label()
         clock = ui.label()
         t = ui.timer(interval=0.1, callback=lambda: clock.set_text(datetime.now().strftime("%X.%f")[:-5]))
         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():
     with ui.row():
         def lazy_update():
         def lazy_update():
@@ -371,12 +370,9 @@ Routed paths must start with a `'/'`.
 with example(add_route):
 with example(add_route):
     import starlette
     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')
     ui.link('Try the new route!', '/new/route')
 
 

+ 65 - 0
nicegui/binding.py

@@ -0,0 +1,65 @@
+#!/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
 import asyncio
 from typing import Awaitable, Callable, Union
 from typing import Awaitable, Callable, Union
-
 import justpy as jp
 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 ..utils import handle_exceptions, provide_arguments, async_provide_arguments
+from .element import Element
 
 
 class Button(Element):
 class Button(Element):
+    text = BindableProperty
 
 
     def __init__(self,
     def __init__(self,
                  text: str = '',
                  text: str = '',
@@ -22,31 +23,27 @@ class Button(Element):
         view = jp.QButton(label=text, color='primary')
         view = jp.QButton(label=text, color='primary')
         super().__init__(view)
         super().__init__(view)
 
 
+        self.text = text
+        self.bind_text_to(self.view, 'label')
+
         if on_click is not None:
         if on_click is not None:
             if asyncio.iscoroutinefunction(on_click):
             if asyncio.iscoroutinefunction(on_click):
                 view.on('click', handle_exceptions(async_provide_arguments(func=on_click, update_function=view.update)))
                 view.on('click', handle_exceptions(async_provide_arguments(func=on_click, update_function=view.update)))
             else:
             else:
                 view.on('click', handle_exceptions(provide_arguments(on_click)))
                 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):
     def set_text(self, text: str):
         self.text = text
         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
         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
         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
         return self

+ 8 - 7
nicegui/elements/element.py

@@ -1,6 +1,6 @@
 import justpy as jp
 import justpy as jp
 from enum import Enum
 from enum import Enum
-from binding.binding import BindableProperty
+from ..binding import bind_from, bind_to, BindableProperty
 from ..globals import view_stack, page_stack
 from ..globals import view_stack, page_stack
 
 
 class Element:
 class Element:
@@ -26,22 +26,23 @@ class Element:
         self.visible_ = visible
         self.visible_ = visible
         (self.view.remove_class if self.visible_ else self.view.set_class)('hidden')
         (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
         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:
         if value is not None:
             def backward(x): return x == value
             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
         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:
         if value is not None:
             def backward(x): return x == value
             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
         return self
 
 
     def classes(self, add: str = '', *, remove: str = '', replace: str = ''):
     def classes(self, add: str = '', *, remove: str = '', replace: str = ''):

+ 12 - 14
nicegui/elements/image.py

@@ -1,7 +1,10 @@
 import justpy as jp
 import justpy as jp
+
+from ..binding import bind_from, bind_to, BindableProperty
 from .group import Group
 from .group import Group
 
 
 class Image(Group):
 class Image(Group):
+    source = BindableProperty
 
 
     def __init__(self,
     def __init__(self,
                  source: str = '',
                  source: str = '',
@@ -13,28 +16,23 @@ class Image(Group):
         :param source: the source of the image; can be an url or a base64 string
         :param source: the source of the image; can be an url or a base64 string
         """
         """
         view = jp.QImg(src=source)
         view = jp.QImg(src=source)
-
         super().__init__(view)
         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):
     def set_source(self, source: str):
         self.source = source
         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
         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
         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
         return self

+ 12 - 14
nicegui/elements/label.py

@@ -1,7 +1,10 @@
 import justpy as jp
 import justpy as jp
+
+from ..binding import bind_from, bind_to, BindableProperty
 from .element import Element
 from .element import Element
 
 
 class Label(Element):
 class Label(Element):
+    text = BindableProperty
 
 
     def __init__(self,
     def __init__(self,
                  text: str = '',
                  text: str = '',
@@ -13,28 +16,23 @@ class Label(Element):
         :param text: the content of the label
         :param text: the content of the label
         """
         """
         view = jp.Div(text=text)
         view = jp.Div(text=text)
-
         super().__init__(view)
         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):
     def set_text(self, text: str):
         self.text = text
         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
         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
         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
         return self

+ 1 - 12
nicegui/elements/svg.py

@@ -1,4 +1,5 @@
 import justpy as jp
 import justpy as jp
+
 from .element import Element
 from .element import Element
 
 
 class Svg(Element):
 class Svg(Element):
@@ -27,15 +28,3 @@ class Svg(Element):
 
 
     def set_content(self, content: str):
     def set_content(self, content: str):
         self.content = content
         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
 import justpy as jp
 from typing import Any, Callable
 from typing import Any, Callable
 import traceback
 import traceback
-from binding import BindableProperty
-from .element import Element
+from ..binding import bind_from, bind_to, BindableProperty
 from ..utils import EventArguments
 from ..utils import EventArguments
+from .element import Element
 
 
 class ValueElement(Element):
 class ValueElement(Element):
     value = BindableProperty()
     value = BindableProperty()
@@ -18,7 +18,7 @@ class ValueElement(Element):
 
 
         self.on_change = on_change
         self.on_change = on_change
         self.value = value
         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):
     def value_to_view(self, value):
         return value
         return value
@@ -35,14 +35,15 @@ class ValueElement(Element):
             except Exception:
             except Exception:
                 traceback.print_exc()
                 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
         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
         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
         return self

+ 2 - 7
nicegui/nicegui.py

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

+ 1 - 1
nicegui/timer.py

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