浏览代码

Merge branch 'main' into feature/open

Christoph Trappe 3 年之前
父节点
当前提交
2b56b1b05c

+ 50 - 0
.github/workflows/test.yml

@@ -0,0 +1,50 @@
+name: Run Tests
+
+on: [push]
+
+jobs:
+  test:
+    strategy:
+      matrix:
+        python: [3.7, 3.8, 3.9]
+      fail-fast: false
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+    steps:
+      - uses: actions/checkout@v2
+      - name: set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python }}
+      - name: set up Poetry
+        uses: abatilo/actions-poetry@v2.0.0
+        with:
+          poetry-version: "1.1.6"
+      - name: install dependencies
+        run: |
+          pip3 install setuptools==57.4.0 # to fix https://github.com/elimintz/justpy/issues/301
+          poetry config virtualenvs.create false --local
+          poetry install
+      - name: test startup
+        run: ./test_startup.sh
+
+  slack:
+    needs:
+      - test
+    if: always() # also execute when test fails
+    runs-on: ubuntu-latest
+    steps:
+      - name: Determine if we need to notify
+        uses: Jimdo/should-i-notify-action@main
+        id: should_notify
+        with:
+          needs_context: ${{ toJson(needs) }}
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+      - name: Slack workflow notification
+        if: steps.should_notify.outputs.should_send_message == 'yes'
+        uses: Gamesight/slack-workflow-status@master
+        with:
+          repo_token: ${{ secrets.GITHUB_TOKEN }}
+          slack_webhook_url: ${{ secrets.SLACK_ROBOTICS_CI_WEBHOOK }}
+          channel: "robotik-ci"
+          name: "NiceGUI"

+ 10 - 8
README.md

@@ -2,8 +2,8 @@
 
 
 <img src="https://raw.githubusercontent.com/zauberzeug/nicegui/main/sceenshots/ui-elements.png" width="300" align="right">
 <img src="https://raw.githubusercontent.com/zauberzeug/nicegui/main/sceenshots/ui-elements.png" width="300" align="right">
 
 
-NiceGUI is an easy to use, Python-based UI framework, which renderes to the web browser.
-You can create buttons, dialogs, markdown, 3D scences, plots and much more.
+NiceGUI is an easy-to-use, Python-based UI framework, which renders to the web browser.
+You can create buttons, dialogs, markdown, 3D scenes, plots and much more.
 
 
 It was designed to be used for micro web apps, dashboards, robotics projects, smart home solutions and similar use cases.
 It was designed to be used for micro web apps, dashboards, robotics projects, smart home solutions and similar use cases.
 It is also helpful for development, for example when tweaking/configuring a machine learning algorithm or tuning motor controllers.
 It is also helpful for development, for example when tweaking/configuring a machine learning algorithm or tuning motor controllers.
@@ -16,7 +16,7 @@ It is also helpful for development, for example when tweaking/configuring a mach
 - standard GUI elements like label, button, checkbox, switch, slider, input, file upload, ...
 - standard GUI elements like label, button, checkbox, switch, slider, input, file upload, ...
 - simple grouping with rows, columns, cards and dialogs
 - simple grouping with rows, columns, cards and dialogs
 - general-purpose HTML and markdown elements
 - general-purpose HTML and markdown elements
-- powerful elements to plot graphs, render 3D scences and get steering events via virtual joysticks
+- powerful elements to plot graphs, render 3D scenes and get steering events via virtual joysticks
 - built-in timer to refresh data in intervals (even every 10 ms)
 - built-in timer to refresh data in intervals (even every 10 ms)
 - straight-forward data binding to write even less code
 - straight-forward data binding to write even less code
 - notifications, dialogs and menus to provide state of the art user interaction
 - notifications, dialogs and menus to provide state of the art user interaction
@@ -46,15 +46,17 @@ Launch it with:
 python3 main.py
 python3 main.py
 ```
 ```
 
 
-The GUI is now avaliable through http://localhost/ in your browser.
+The GUI is now available through http://localhost:8080/ in your browser.
 Note: The script will automatically reload the page when you modify the code.
 Note: The script will automatically reload the page when you modify the code.
 
 
+Full documentation can be found at [https://nicegui.io](https://nicegui.io).
+
 ## Configuration
 ## Configuration
 
 
 You can call `ui.run()` with optional arguments for some high-level configuration:
 You can call `ui.run()` with optional arguments for some high-level configuration:
 
 
 - `host` (default: `'0.0.0.0'`)
 - `host` (default: `'0.0.0.0'`)
-- `port` (default: `80`)
+- `port` (default: `8080`)
 - `title` (default: `'NiceGUI'`)
 - `title` (default: `'NiceGUI'`)
 - `favicon` (default: `'favicon.ico'`)
 - `favicon` (default: `'favicon.ico'`)
 - `reload`: automatically reload the ui on file changes (default: `True`)
 - `reload`: automatically reload the ui on file changes (default: `True`)
@@ -67,7 +69,7 @@ You can call `ui.run()` with optional arguments for some high-level configuratio
 Use the [multi-arch docker image](https://hub.docker.com/repository/docker/zauberzeug/nicegui) for pain-free installation:
 Use the [multi-arch docker image](https://hub.docker.com/repository/docker/zauberzeug/nicegui) for pain-free installation:
 
 
 ```bash
 ```bash
-docker run --rm -p 8888:80 -v $(pwd)/my_script.py:/app/main.py -it zauberzeug/nicegui:latest
+docker run --rm -p 8888:8080 -v $(pwd)/my_script.py:/app/main.py -it zauberzeug/nicegui:latest
 ```
 ```
 
 
 This will start the server at http://localhost:8888 with code from `my_script.py` within the current directory.
 This will start the server at http://localhost:8888 with code from `my_script.py` within the current directory.
@@ -76,10 +78,10 @@ Code modification triggers an automatic reload.
 ## Why?
 ## Why?
 
 
 We like [Streamlit](https://streamlit.io/) but find it does [too much magic when it comes to state handling](https://github.com/zauberzeug/nicegui/issues/1#issuecomment-847413651).
 We like [Streamlit](https://streamlit.io/) but find it does [too much magic when it comes to state handling](https://github.com/zauberzeug/nicegui/issues/1#issuecomment-847413651).
-In search for an alernative nice library to write simple graphical user interfaces in Python we discovered [justpy](https://justpy.io/).
+In search for an alternative nice library to write simple graphical user interfaces in Python we discovered [justpy](https://justpy.io/).
 While too "low-level HTML" for our daily usage it provides a great basis for "NiceGUI".
 While too "low-level HTML" for our daily usage it provides a great basis for "NiceGUI".
 
 
 ## API
 ## API
 
 
 The API reference is hosted at [https://nicegui.io](https://nicegui.io) and is [implemented with NiceGUI itself](https://github.com/zauberzeug/nicegui/blob/main/main.py).
 The API reference is hosted at [https://nicegui.io](https://nicegui.io) and is [implemented with NiceGUI itself](https://github.com/zauberzeug/nicegui/blob/main/main.py).
-You should also have a look at [examples.py](https://github.com/zauberzeug/nicegui/tree/main/examples.py) for an extensive demonstration of what you can do with NiceGUI.
+You may also have a look at [examples.py](https://github.com/zauberzeug/nicegui/tree/main/examples.py) for more demonstrations of what you can do with NiceGUI.

+ 4 - 1
main.py

@@ -39,7 +39,10 @@ def example(content: Union[Element, str]):
         callFrame = inspect.currentframe().f_back.f_back
         callFrame = inspect.currentframe().f_back.f_back
         end = callFrame.f_lineno
         end = callFrame.f_lineno
         code = inspect.getsource(sys.modules[__name__])
         code = inspect.getsource(sys.modules[__name__])
-        code = code.splitlines()[begin:end]
+        lines = code.splitlines()
+        while lines[end]:
+            end += 1
+        code = lines[begin:end]
         code = [l[4:] for l in code]
         code = [l[4:] for l in code]
         code.insert(0, '```python')
         code.insert(0, '```python')
         code.insert(1, 'from nicegui import ui')
         code.insert(1, 'from nicegui import ui')

+ 2 - 2
nicegui/config.py

@@ -7,7 +7,7 @@ from . import globals
 class Config(BaseModel):
 class Config(BaseModel):
     # NOTE: should be in sync with ui.run arguments
     # NOTE: should be in sync with ui.run arguments
     host: str = '0.0.0.0'
     host: str = '0.0.0.0'
-    port: int = 80
+    port: int = 8080
     title: str = 'NiceGUI'
     title: str = 'NiceGUI'
     favicon: str = 'favicon.ico'
     favicon: str = 'favicon.ico'
     reload: bool = True
     reload: bool = True
@@ -34,7 +34,7 @@ try:
     with open(filepath) as f:
     with open(filepath) as f:
         source = f.read()
         source = f.read()
 except FileNotFoundError:
 except FileNotFoundError:
-    print('Could not main script. Starting with interactive mode.', flush=True)
+    print('Could not find main script. Starting with interactive mode.', flush=True)
     config = Config(interactive=True)
     config = Config(interactive=True)
 else:
 else:
     for node in ast.walk(ast.parse(source)):
     for node in ast.walk(ast.parse(source)):

+ 3 - 3
nicegui/elements/choice_element.py

@@ -1,17 +1,17 @@
 import justpy as jp
 import justpy as jp
-from typing import Any, Awaitable, Callable, Optional, Union
+from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
 from .value_element import ValueElement
 from .value_element import ValueElement
 
 
 class ChoiceElement(ValueElement):
 class ChoiceElement(ValueElement):
 
 
     def __init__(self,
     def __init__(self,
                  view: jp.HTMLBaseComponent,
                  view: jp.HTMLBaseComponent,
-                 options: Union[list, dict],
+                 options: Union[List, Dict],
                  *,
                  *,
                  value: Any,
                  value: Any,
                  on_change: Optional[Union[Callable, Awaitable]] = None,
                  on_change: Optional[Union[Callable, Awaitable]] = None,
                  ):
                  ):
-        if isinstance(options, list):
+        if isinstance(options, List):
             view.options = [{'label': option, 'value': option} for option in options]
             view.options = [{'label': option, 'value': option} for option in options]
         else:
         else:
             view.options = [{'label': value, 'value': key} for key, value in options.items()]
             view.options = [{'label': value, 'value': key} for key, value in options.items()]

+ 2 - 1
nicegui/elements/group.py

@@ -1,4 +1,5 @@
 from __future__ import annotations
 from __future__ import annotations
+from typing import List
 import justpy as jp
 import justpy as jp
 from ..globals import view_stack
 from ..globals import view_stack
 from ..binding import active_links, bindings, bindable_properties
 from ..binding import active_links, bindings, bindable_properties
@@ -17,7 +18,7 @@ class Group(Element):
         return self.classes(replace='').style(replace='')
         return self.classes(replace='').style(replace='')
 
 
     def clear(self):
     def clear(self):
-        def collect_components(view: jp.HTMLBaseComponent) -> list[jp.HTMLBaseComponent]:
+        def collect_components(view: jp.HTMLBaseComponent) -> List[jp.HTMLBaseComponent]:
             return view.components + [view for child in view.components for view in collect_components(child)]
             return view.components + [view for child in view.components for view in collect_components(child)]
         components = collect_components(self.view)
         components = collect_components(self.view)
 
 

+ 2 - 2
nicegui/elements/joystick.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Callable, Dict, Optional
 from .custom_view import CustomView
 from .custom_view import CustomView
 from .element import Element
 from .element import Element
 
 
@@ -43,7 +43,7 @@ class Joystick(Element):
                  on_start: Optional[Callable] = None,
                  on_start: Optional[Callable] = None,
                  on_move: Optional[Callable] = None,
                  on_move: Optional[Callable] = None,
                  on_end: Optional[Callable] = None,
                  on_end: Optional[Callable] = None,
-                 **options: dict,
+                 **options: Dict,
                  ):
                  ):
         """Joystick
         """Joystick
 
 

+ 2 - 2
nicegui/elements/keyboard.py

@@ -1,5 +1,5 @@
 import traceback
 import traceback
-from typing import Awaitable, Callable, Optional, Union
+from typing import Awaitable, Callable, Dict, Optional, Union
 
 
 from ..events import KeyEventArguments, KeyboardAction, KeyboardKey, KeyboardModifiers, handle_event
 from ..events import KeyEventArguments, KeyboardAction, KeyboardKey, KeyboardModifiers, handle_event
 from .custom_view import CustomView
 from .custom_view import CustomView
@@ -35,7 +35,7 @@ class Keyboard(Element):
         self.active = active
         self.active = active
         self.key_handler = on_key
         self.key_handler = on_key
 
 
-    def handle_key(self, msg: dict):
+    def handle_key(self, msg: Dict):
         if not self.active:
         if not self.active:
             return
             return
 
 

+ 6 - 3
nicegui/elements/log.py

@@ -1,3 +1,5 @@
+from __future__ import annotations
+from typing import Deque
 import asyncio
 import asyncio
 import traceback
 import traceback
 import urllib
 import urllib
@@ -5,10 +7,11 @@ from collections import deque
 from justpy.htmlcomponents import WebPage
 from justpy.htmlcomponents import WebPage
 from .custom_view import CustomView
 from .custom_view import CustomView
 from .element import Element
 from .element import Element
+from ..task_logger import create_task
 
 
 class LogView(CustomView):
 class LogView(CustomView):
 
 
-    def __init__(self, lines: deque[str], max_lines: int):
+    def __init__(self, lines: Deque[str], max_lines: int):
         super().__init__('log', __file__, max_lines=max_lines)
         super().__init__('log', __file__, max_lines=max_lines)
         self.lines = lines
         self.lines = lines
         self.allowed_events = ['onConnect']
         self.allowed_events = ['onConnect']
@@ -19,7 +22,7 @@ class LogView(CustomView):
             if self.lines:
             if self.lines:
                 content = '\n'.join(self.lines)
                 content = '\n'.join(self.lines)
                 command = f'push("{urllib.parse.quote(content)}")'
                 command = f'push("{urllib.parse.quote(content)}")'
-                asyncio.get_event_loop().create_task(self.run_method(command, msg.websocket))
+                create_task(self.run_method(command, msg.websocket), name=str(command))
         except:
         except:
             traceback.print_exc()
             traceback.print_exc()
 
 
@@ -44,4 +47,4 @@ class Log(Element):
         ])
         ])
 
 
     def push(self, line: str):
     def push(self, line: str):
-        asyncio.get_event_loop().create_task(self.push_async(line))
+        create_task(self.push_async(line), name=f'log.push line {line}')

+ 2 - 2
nicegui/elements/notify.py

@@ -1,6 +1,6 @@
 import justpy as jp
 import justpy as jp
 from .element import Element
 from .element import Element
-import asyncio
+from ..task_logger import create_task
 
 
 
 
 class Notify(Element):
 class Notify(Element):
@@ -22,7 +22,7 @@ class Notify(Element):
         view = jp.QNotify(message=message, position=position, closeBtn=close_button)
         view = jp.QNotify(message=message, position=position, closeBtn=close_button)
 
 
         super().__init__(view)
         super().__init__(view)
-        asyncio.get_event_loop().create_task(self.notify_async())
+        create_task(self.notify_async(), name='notify_async')
 
 
     async def notify_async(self):
     async def notify_async(self):
         self.view.notify = True
         self.view.notify = True

+ 2 - 2
nicegui/elements/radio.py

@@ -1,11 +1,11 @@
 import justpy as jp
 import justpy as jp
-from typing import Awaitable, Callable, Optional, Union
+from typing import Awaitable, Callable, Dict, List, Optional, Union
 from .choice_element import ChoiceElement
 from .choice_element import ChoiceElement
 
 
 class Radio(ChoiceElement):
 class Radio(ChoiceElement):
 
 
     def __init__(self,
     def __init__(self,
-                 options: Union[list, dict],
+                 options: Union[List, Dict],
                  *,
                  *,
                  value: any = None,
                  value: any = None,
                  on_change: Optional[Union[Callable, Awaitable]] = None,
                  on_change: Optional[Union[Callable, Awaitable]] = None,

+ 5 - 4
nicegui/elements/scene_object3d.py

@@ -1,12 +1,13 @@
 from __future__ import annotations
 from __future__ import annotations
 import asyncio
 import asyncio
-from typing import Optional
+from typing import List, Optional
 import uuid
 import uuid
 import numpy as np
 import numpy as np
 from justpy.htmlcomponents import WebPage
 from justpy.htmlcomponents import WebPage
+from ..task_logger import create_task
 
 
 class Object3D:
 class Object3D:
-    stack: list[Object3D] = []
+    stack: List[Object3D] = []
 
 
     def __init__(self, type: str, *args):
     def __init__(self, type: str, *args):
         self.type = type
         self.type = type
@@ -35,7 +36,7 @@ class Object3D:
     def run_command(self, command: str, socket=None):
     def run_command(self, command: str, socket=None):
         sockets = [socket] if socket else WebPage.sockets.get(self.page.page_id, {}).values()
         sockets = [socket] if socket else WebPage.sockets.get(self.page.page_id, {}).values()
         for socket in sockets:
         for socket in sockets:
-            asyncio.get_event_loop().create_task(self.view.run_method(command, socket))
+            create_task(self.view.run_method(command, socket), name=command)
 
 
     def send_to(self, socket):
     def send_to(self, socket):
         self.run_command(self._create_command, socket)
         self.run_command(self._create_command, socket)
@@ -96,7 +97,7 @@ class Object3D:
         Rz = np.array([[np.cos(kappa), -np.sin(kappa), 0], [np.sin(kappa), np.cos(kappa), 0], [0, 0, 1]])
         Rz = np.array([[np.cos(kappa), -np.sin(kappa), 0], [np.sin(kappa), np.cos(kappa), 0], [0, 0, 1]])
         return self.rotate_R((Rz @ Ry @ Rx).tolist())
         return self.rotate_R((Rz @ Ry @ Rx).tolist())
 
 
-    def rotate_R(self, R: list[list[float]]):
+    def rotate_R(self, R: List[List[float]]):
         if self.R != R:
         if self.R != R:
             self.R = R
             self.R = R
             self.run_command(self._rotate_command)
             self.run_command(self._rotate_command)

+ 9 - 9
nicegui/elements/scene_objects.py

@@ -1,5 +1,5 @@
 from __future__ import annotations
 from __future__ import annotations
-from typing import Optional
+from typing import List, Optional
 from .scene_object3d import Object3D
 from .scene_object3d import Object3D
 
 
 class Scene(Object3D):
 class Scene(Object3D):
@@ -47,7 +47,7 @@ class Cylinder(Object3D):
 class Extrusion(Object3D):
 class Extrusion(Object3D):
 
 
     def __init__(self,
     def __init__(self,
-                 outline: list[list[float, float]],
+                 outline: List[List[float, float]],
                  height: float,
                  height: float,
                  wireframe: bool = False,
                  wireframe: bool = False,
                  ):
                  ):
@@ -64,18 +64,18 @@ class Stl(Object3D):
 class Line(Object3D):
 class Line(Object3D):
 
 
     def __init__(self,
     def __init__(self,
-                 start: list[float, float, float],
-                 end: list[float, float, float],
+                 start: List[float, float, float],
+                 end: List[float, float, float],
                  ):
                  ):
         super().__init__('line', start, end)
         super().__init__('line', start, end)
 
 
 class Curve(Object3D):
 class Curve(Object3D):
 
 
     def __init__(self,
     def __init__(self,
-                 start: list[float, float, float],
-                 control1: list[float, float, float],
-                 control2: list[float, float, float],
-                 end: list[float, float, float],
+                 start: List[float, float, float],
+                 control1: List[float, float, float],
+                 control2: List[float, float, float],
+                 end: List[float, float, float],
                  num_points: int = 20,
                  num_points: int = 20,
                  ):
                  ):
         super().__init__('curve', start, control1, control2, end, num_points)
         super().__init__('curve', start, control1, control2, end, num_points)
@@ -84,6 +84,6 @@ class Texture(Object3D):
 
 
     def __init__(self,
     def __init__(self,
                  url: str,
                  url: str,
-                 coordinates: list[list[Optional[list[float]]]],
+                 coordinates: List[List[Optional[List[float]]]],
                  ):
                  ):
         super().__init__('texture', url, coordinates)
         super().__init__('texture', url, coordinates)

+ 2 - 2
nicegui/elements/select.py

@@ -1,11 +1,11 @@
 import justpy as jp
 import justpy as jp
-from typing import Awaitable, Callable, Optional, Union
+from typing import Awaitable, Callable, Dict, List, Optional, Union
 from .choice_element import ChoiceElement
 from .choice_element import ChoiceElement
 
 
 class Select(ChoiceElement):
 class Select(ChoiceElement):
 
 
     def __init__(self,
     def __init__(self,
-                 options: Union[list, dict],
+                 options: Union[List, Dict],
                  *,
                  *,
                  value: any = None,
                  value: any = None,
                  on_change: Optional[Union[Callable, Awaitable]] = None,
                  on_change: Optional[Union[Callable, Awaitable]] = None,

+ 2 - 2
nicegui/elements/toggle.py

@@ -1,11 +1,11 @@
 import justpy as jp
 import justpy as jp
-from typing import Awaitable, Callable, Optional, Union
+from typing import Awaitable, Callable, Dict, List, Optional, Union
 from .choice_element import ChoiceElement
 from .choice_element import ChoiceElement
 
 
 class Toggle(ChoiceElement):
 class Toggle(ChoiceElement):
 
 
     def __init__(self,
     def __init__(self,
-                 options: Union[list, dict],
+                 options: Union[List, Dict],
                  *,
                  *,
                  value: any = None,
                  value: any = None,
                  on_change: Optional[Union[Callable, Awaitable]] = None,
                  on_change: Optional[Union[Callable, Awaitable]] = None,

+ 2 - 1
nicegui/events.py

@@ -5,6 +5,7 @@ import traceback
 from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
 from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
 
 
 from .elements.element import Element
 from .elements.element import Element
+from .task_logger import create_task
 
 
 class EventArguments(BaseModel):
 class EventArguments(BaseModel):
     class Config:
     class Config:
@@ -209,7 +210,7 @@ def handle_event(handler: Optional[Union[Callable, Awaitable]], arguments: Event
                     await result
                     await result
                 except Exception:
                 except Exception:
                     traceback.print_exc()
                     traceback.print_exc()
-            asyncio.get_event_loop().create_task(async_handler())
+            create_task(async_handler(), name=str(handler))
             return False
             return False
         else:
         else:
             return result
             return result

+ 4 - 4
nicegui/globals.py

@@ -1,7 +1,7 @@
 from __future__ import annotations
 from __future__ import annotations
 import asyncio
 import asyncio
 import logging
 import logging
-from typing import TYPE_CHECKING
+from typing import List, TYPE_CHECKING
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from starlette.applications import Starlette
     from starlette.applications import Starlette
     import justpy as jp
     import justpy as jp
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
 
 
 app: 'Starlette'
 app: 'Starlette'
 config: 'Config'
 config: 'Config'
-page_stack: list['Page'] = []
-view_stack: list['jp.HTMLBaseComponent'] = []
-tasks: list[asyncio.tasks.Task] = []
+page_stack: List['Page'] = []
+view_stack: List['jp.HTMLBaseComponent'] = []
+tasks: List[asyncio.tasks.Task] = []
 log: logging.Logger = logging.getLogger('nicegui')
 log: logging.Logger = logging.getLogger('nicegui')

+ 1 - 4
nicegui/nicegui.py

@@ -7,12 +7,9 @@ import justpy as jp
 from .timer import Timer
 from .timer import Timer
 from . import globals
 from . import globals
 from . import binding
 from . import binding
+from .task_logger import create_task
 
 
 
 
-def create_task(coro, name: str) -> asyncio.tasks.Task:
-    loop = asyncio.get_event_loop()
-    return loop.create_task(coro, name=name)
-
 @jp.app.on_event('startup')
 @jp.app.on_event('startup')
 def startup():
 def startup():
     globals.tasks.extend(create_task(t.coro, name=t.name) for t in Timer.prepared_coroutines)
     globals.tasks.extend(create_task(t.coro, name=t.name) for t in Timer.prepared_coroutines)

+ 1 - 1
nicegui/run.py

@@ -14,7 +14,7 @@ if not globals.config.interactive and globals.config.reload and not inspect.stac
 
 
 def run(self, *,
 def run(self, *,
         host: str = '0.0.0.0',
         host: str = '0.0.0.0',
-        port: int = 80,
+        port: int = 8080,
         title: str = 'NiceGUI',
         title: str = 'NiceGUI',
         favicon: str = 'favicon.ico',
         favicon: str = 'favicon.ico',
         reload: bool = True,
         reload: bool = True,

+ 55 - 0
nicegui/task_logger.py

@@ -0,0 +1,55 @@
+'''original copied from https://quantlane.com/blog/ensure-asyncio-task-exceptions-get-logged/'''
+
+from typing import Any, Awaitable, Optional, Tuple, TypeVar
+
+import asyncio
+import functools
+import logging
+import sys
+
+T = TypeVar('T')
+
+
+def create_task(
+    coroutine: Awaitable[T],
+    *,
+    loop: Optional[asyncio.AbstractEventLoop] = None,
+    name: str = 'unnamed task',
+) -> 'asyncio.Task[T]':  # This type annotation has to be quoted for Python < 3.9, see https://www.python.org/dev/peps/pep-0585/
+    '''
+    This helper function wraps a ``loop.create_task(coroutine())`` call and ensures there is
+    an exception handler added to the resulting task. If the task raises an exception it is logged
+    using the provided ``logger``, with additional context provided by ``message`` and optionally
+    ``message_args``.
+    '''
+
+    logger = logging.getLogger(__name__)
+    message = 'Task raised an exception'
+    message_args = ()
+    if loop is None:
+        loop = asyncio.get_running_loop()
+    if sys.version_info[1] < 8:
+        task = loop.create_task(coroutine)  # name parameter is only supported from 3.8 onward
+    else:
+        task = loop.create_task(coroutine, name=name)
+    task.add_done_callback(
+        functools.partial(_handle_task_result, logger=logger, message=message, message_args=message_args)
+    )
+    return task
+
+
+def _handle_task_result(
+    task: asyncio.Task,
+    *,
+    logger: logging.Logger,
+    message: str,
+    message_args: Tuple[Any, ...] = (),
+) -> None:
+    try:
+        task.result()
+    except asyncio.CancelledError:
+        pass  # Task cancellation should not be logged as an error.
+    # Ad the pylint ignore: we want to handle all exceptions here so that the result of the task
+    # is properly logged. There is no point re-raising the exception in this callback.
+    except Exception:  # pylint: disable=broad-except
+        logger.exception(message, *message_args)

+ 2 - 1
nicegui/timer.py

@@ -6,6 +6,7 @@ from collections import namedtuple
 
 
 from .binding import BindableProperty
 from .binding import BindableProperty
 from .globals import tasks, view_stack
 from .globals import tasks, view_stack
+from .task_logger import create_task
 
 
 NamedCoroutine = namedtuple('NamedCoroutine', ['name', 'coro'])
 NamedCoroutine = namedtuple('NamedCoroutine', ['name', 'coro'])
 
 
@@ -65,4 +66,4 @@ class Timer:
         if not event_loop.is_running():
         if not event_loop.is_running():
             self.prepared_coroutines.append(NamedCoroutine(str(callback), coroutine))
             self.prepared_coroutines.append(NamedCoroutine(str(callback), coroutine))
         else:
         else:
-            tasks.append(event_loop.create_task(coroutine, name=str(callback)))
+            tasks.append(create_task(coroutine, name=str(callback)))

+ 34 - 0
test_startup.sh

@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+run() {
+    output=`{ timeout 10 python3 $1; } 2>&1`
+    exitcode=$?
+    test $exitcode -eq 124 && exitcode=0 # exitcode 124 is comming from "timeout command above"
+    echo $output | grep "JustPy ready to go" > /dev/null || exitcode=1
+    echo $output | grep "Traceback" > /dev/null && exitcode=1
+    echo $output | grep "Error" > /dev/null && exitcode=1
+    if test $exitcode -ne 0; then
+        echo "wrong exit code $exitcode. Output was:"
+        echo $output
+        return 1
+    fi
+}
+
+check() {
+    echo checking $1 ----------
+    pushd $(dirname "$1") >/dev/null
+    if run $(basename "$1"); then
+        echo "ok --------"
+        popd > /dev/null
+    else
+        echo "failed -------"
+        popd > /dev/null
+        return 1
+    fi
+}
+
+success=0
+check main.py || success=1
+check examples.py || success=1
+echo exit $success
+test $success -eq 0