Prechádzať zdrojové kódy

move "run executor" into run.py

Falko Schindler 1 rok pred
rodič
commit
8c5c345e63

+ 1 - 3
nicegui/__init__.py

@@ -1,6 +1,4 @@
-from . import ui  # pylint: disable=redefined-builtin
-from . import context, elements, globals, optional_features  # pylint: disable=redefined-builtin
-from . import run_executor as run
+from . import context, elements, optional_features, run, ui
 from .api_router import APIRouter
 from .api_router import APIRouter
 from .awaitable_response import AwaitableResponse
 from .awaitable_response import AwaitableResponse
 from .client import Client
 from .client import Client

+ 2 - 2
nicegui/native/native.py

@@ -3,8 +3,8 @@ import warnings
 from multiprocessing import Queue
 from multiprocessing import Queue
 from typing import Any, Callable, Tuple
 from typing import Any, Callable, Tuple
 
 
+from .. import run
 from ..logging import log
 from ..logging import log
-from ..run_executor import io_bound
 
 
 method_queue: Queue = Queue()
 method_queue: Queue = Queue()
 response_queue: Queue = Queue()
 response_queue: Queue = Queue()
@@ -120,7 +120,7 @@ try:
                     log.exception(f'error in {name}')
                     log.exception(f'error in {name}')
                     return None
                     return None
             name = inspect.currentframe().f_back.f_code.co_name  # type: ignore
             name = inspect.currentframe().f_back.f_code.co_name  # type: ignore
-            return await io_bound(wrapper, *args, **kwargs)
+            return await run.io_bound(wrapper, *args, **kwargs)
 
 
         def signal_server_shutdown(self) -> None:
         def signal_server_shutdown(self) -> None:
             """Signal the server shutdown."""
             """Signal the server shutdown."""

+ 3 - 3
nicegui/nicegui.py

@@ -10,8 +10,8 @@ from fastapi.responses import FileResponse, Response
 from fastapi.staticfiles import StaticFiles
 from fastapi.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 from fastapi_socketio import SocketManager
 
 
-from . import (air, background_tasks, binding, favicon, globals, json, outbox,  # pylint: disable=redefined-builtin
-               run_executor, welcome)
+from . import (air, background_tasks, binding, favicon, globals, json, outbox, run,  # pylint: disable=redefined-builtin
+               welcome)
 from .app import App
 from .app import App
 from .client import Client
 from .client import Client
 from .dependencies import js_components, libraries
 from .dependencies import js_components, libraries
@@ -108,7 +108,7 @@ async def handle_shutdown() -> None:
     if app.native.main_window:
     if app.native.main_window:
         app.native.main_window.signal_server_shutdown()
         app.native.main_window.signal_server_shutdown()
     globals.app.stop()
     globals.app.stop()
-    run_executor.tear_down()
+    run.tear_down()
     air.disconnect()
     air.disconnect()
 
 
 
 

+ 32 - 157
nicegui/run.py

@@ -1,169 +1,44 @@
-import multiprocessing
-import os
+import asyncio
 import sys
 import sys
-from pathlib import Path
-from typing import Any, List, Literal, Optional, Tuple, Union
+from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
+from functools import partial
+from typing import Any, Callable
 
 
-import __main__
-from starlette.routing import Route
-from uvicorn.main import STARTUP_FAILURE
-from uvicorn.supervisors import ChangeReload, Multiprocess
+from . import globals, helpers  # pylint: disable=redefined-builtin
 
 
-from . import air, globals, helpers  # pylint: disable=redefined-builtin
-from . import native as native_module
-from .client import Client
-from .language import Language
-from .logging import log
-from .server import CustomServerConfig, Server
+process_pool = ProcessPoolExecutor()
+thread_pool = ThreadPoolExecutor()
 
 
-APP_IMPORT_STRING = 'nicegui:app'
 
 
-
-def run(*,
-        host: Optional[str] = None,
-        port: int = 8080,
-        title: str = 'NiceGUI',
-        viewport: str = 'width=device-width, initial-scale=1',
-        favicon: Optional[Union[str, Path]] = None,
-        dark: Optional[bool] = False,
-        language: Language = 'en-US',
-        binding_refresh_interval: float = 0.1,
-        reconnect_timeout: float = 3.0,
-        show: bool = True,
-        on_air: Optional[Union[str, Literal[True]]] = None,
-        native: bool = False,
-        window_size: Optional[Tuple[int, int]] = None,
-        fullscreen: bool = False,
-        frameless: bool = False,
-        reload: bool = True,
-        uvicorn_logging_level: str = 'warning',
-        uvicorn_reload_dirs: str = '.',
-        uvicorn_reload_includes: str = '*.py',
-        uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
-        tailwind: bool = True,
-        prod_js: bool = True,
-        endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
-        storage_secret: Optional[str] = None,
-        **kwargs: Any,
-        ) -> None:
-    '''ui.run
-
-    You can call `ui.run()` with optional arguments:
-
-    :param host: start server with this host (defaults to `'127.0.0.1` in native mode, otherwise `'0.0.0.0'`)
-    :param port: use this port (default: `8080`)
-    :param title: page title (default: `'NiceGUI'`, can be overwritten per page)
-    :param viewport: page meta viewport content (default: `'width=device-width, initial-scale=1'`, can be overwritten per page)
-    :param favicon: relative filepath, absolute URL to a favicon (default: `None`, NiceGUI icon will be used) or emoji (e.g. `'🚀'`, works for most browsers)
-    :param dark: whether to use Quasar's dark mode (default: `False`, use `None` for "auto" mode)
-    :param language: language for Quasar elements (default: `'en-US'`)
-    :param binding_refresh_interval: time between binding updates (default: `0.1` seconds, bigger is more CPU friendly)
-    :param reconnect_timeout: maximum time the server waits for the browser to reconnect (default: 3.0 seconds)
-    :param show: automatically open the UI in a browser tab (default: `True`)
-    :param on_air: tech preview: `allows temporary remote access <https://nicegui.io/documentation#nicegui_on_air>`_ if set to `True` (default: disabled)
-    :param native: open the UI in a native window of size 800x600 (default: `False`, deactivates `show`, automatically finds an open port)
-    :param window_size: open the UI in a native window with the provided size (e.g. `(1024, 786)`, default: `None`, also activates `native`)
-    :param fullscreen: open the UI in a fullscreen window (default: `False`, also activates `native`)
-    :param frameless: open the UI in a frameless window (default: `False`, also activates `native`)
-    :param reload: automatically reload the UI on file changes (default: `True`)
-    :param uvicorn_logging_level: logging level for uvicorn server (default: `'warning'`)
-    :param uvicorn_reload_dirs: string with comma-separated list for directories to be monitored (default is current working directory only)
-    :param uvicorn_reload_includes: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'.py'`)
-    :param uvicorn_reload_excludes: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
-    :param tailwind: whether to use Tailwind (experimental, default: `True`)
-    :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
-    :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
-    :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
-    :param kwargs: additional keyword arguments are passed to `uvicorn.run`    
-    '''
-    globals.ui_run_has_been_called = True
-    globals.reload = reload
-    globals.title = title
-    globals.viewport = viewport
-    globals.favicon = favicon
-    globals.dark = dark
-    globals.language = language
-    globals.binding_refresh_interval = binding_refresh_interval
-    globals.reconnect_timeout = reconnect_timeout
-    globals.tailwind = tailwind
-    globals.prod_js = prod_js
-    globals.endpoint_documentation = endpoint_documentation
-
-    for route in globals.app.routes:
-        if not isinstance(route, Route):
-            continue
-        if route.path.startswith('/_nicegui') and hasattr(route, 'methods'):
-            route.include_in_schema = endpoint_documentation in {'internal', 'all'}
-        if route.path == '/' or route.path in Client.page_routes.values():
-            route.include_in_schema = endpoint_documentation in {'page', 'all'}
-
-    if on_air:
-        air.instance = air.Air('' if on_air is True else on_air)
-
-    if multiprocessing.current_process().name != 'MainProcess':
+async def _run(executor: Any, callback: Callable, *args: Any, **kwargs: Any) -> Any:
+    if globals.app.is_stopping:
         return
         return
+    try:
+        loop = asyncio.get_running_loop()
+        return await loop.run_in_executor(executor, partial(callback, *args, **kwargs))
+    except RuntimeError as e:
+        if 'cannot schedule new futures after shutdown' not in str(e):
+            raise
+    except asyncio.exceptions.CancelledError:
+        pass
 
 
-    if reload and not hasattr(__main__, '__file__'):
-        log.warning('auto-reloading is only supported when running from a file')
-        globals.reload = reload = False
 
 
-    if fullscreen:
-        native = True
-    if frameless:
-        native = True
-    if window_size:
-        native = True
-    if native:
-        show = False
-        host = host or '127.0.0.1'
-        port = native_module.find_open_port()
-        width, height = window_size or (800, 600)
-        native_module.activate(host, port, title, width, height, fullscreen, frameless)
-    else:
-        host = host or '0.0.0.0'
+async def cpu_bound(callback: Callable, *args: Any, **kwargs: Any) -> Any:
+    """Run a CPU-bound function in a separate process."""
+    return await _run(process_pool, callback, *args, **kwargs)
 
 
-    # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
-    os.environ['NICEGUI_HOST'] = host
-    os.environ['NICEGUI_PORT'] = str(port)
 
 
-    if show:
-        helpers.schedule_browser(host, port)
+async def io_bound(callback: Callable, *args: Any, **kwargs: Any) -> Any:
+    """Run an I/O-bound function in a separate thread."""
+    return await _run(thread_pool, callback, *args, **kwargs)
 
 
-    def split_args(args: str) -> List[str]:
-        return [a.strip() for a in args.split(',')]
 
 
-    # NOTE: The following lines are basically a copy of `uvicorn.run`, but keep a reference to the `server`.
-
-    config = CustomServerConfig(
-        APP_IMPORT_STRING if reload else globals.app,
-        host=host,
-        port=port,
-        reload=reload,
-        reload_includes=split_args(uvicorn_reload_includes) if reload else None,
-        reload_excludes=split_args(uvicorn_reload_excludes) if reload else None,
-        reload_dirs=split_args(uvicorn_reload_dirs) if reload else None,
-        log_level=uvicorn_logging_level,
-        **kwargs,
-    )
-    config.storage_secret = storage_secret
-    config.method_queue = native_module.method_queue if native else None
-    config.response_queue = native_module.response_queue if native else None
-    Server.create_singleton(config)
-
-    if (reload or config.workers > 1) and not isinstance(config.app, str):
-        log.warning('You must pass the application as an import string to enable "reload" or "workers".')
-        sys.exit(1)
-
-    if config.should_reload:
-        sock = config.bind_socket()
-        ChangeReload(config, target=Server.instance.run, sockets=[sock]).run()
-    elif config.workers > 1:
-        sock = config.bind_socket()
-        Multiprocess(config, target=Server.instance.run, sockets=[sock]).run()
-    else:
-        Server.instance.run()
-    if config.uds:
-        os.remove(config.uds)  # pragma: py-win32
-
-    if not Server.instance.started and not config.should_reload and config.workers == 1:
-        sys.exit(STARTUP_FAILURE)
+def tear_down() -> None:
+    """Kill all processes and threads."""
+    if helpers.is_pytest():
+        return
+    for p in process_pool._processes.values():  # pylint: disable=protected-access
+        p.kill()
+    kwargs = {'cancel_futures': True} if sys.version_info >= (3, 9) else {}
+    process_pool.shutdown(wait=True, **kwargs)
+    thread_pool.shutdown(wait=False, **kwargs)

+ 0 - 44
nicegui/run_executor.py

@@ -1,44 +0,0 @@
-import asyncio
-import sys
-from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
-from functools import partial
-from typing import Any, Callable
-
-from . import globals, helpers  # pylint: disable=redefined-builtin
-
-process_pool = ProcessPoolExecutor()
-thread_pool = ThreadPoolExecutor()
-
-
-async def _run(executor: Any, callback: Callable, *args: Any, **kwargs: Any) -> Any:
-    if globals.app.is_stopping:
-        return
-    try:
-        loop = asyncio.get_running_loop()
-        return await loop.run_in_executor(executor, partial(callback, *args, **kwargs))
-    except RuntimeError as e:
-        if 'cannot schedule new futures after shutdown' not in str(e):
-            raise
-    except asyncio.exceptions.CancelledError:
-        pass
-
-
-async def cpu_bound(callback: Callable, *args: Any, **kwargs: Any) -> Any:
-    """Run a CPU-bound function in a separate process."""
-    return await _run(process_pool, callback, *args, **kwargs)
-
-
-async def io_bound(callback: Callable, *args: Any, **kwargs: Any) -> Any:
-    """Run an I/O-bound function in a separate thread."""
-    return await _run(thread_pool, callback, *args, **kwargs)
-
-
-def tear_down() -> None:
-    """Kill all processes and threads."""
-    if helpers.is_pytest():
-        return
-    for p in process_pool._processes.values():  # pylint: disable=protected-access
-        p.kill()
-    kwargs = {'cancel_futures': True} if sys.version_info >= (3, 9) else {}
-    process_pool.shutdown(wait=True, **kwargs)
-    thread_pool.shutdown(wait=False, **kwargs)

+ 2 - 2
nicegui/ui.py

@@ -191,5 +191,5 @@ from .page_layout import Header as header
 from .page_layout import LeftDrawer as left_drawer
 from .page_layout import LeftDrawer as left_drawer
 from .page_layout import PageSticky as page_sticky
 from .page_layout import PageSticky as page_sticky
 from .page_layout import RightDrawer as right_drawer
 from .page_layout import RightDrawer as right_drawer
-from .run import run
-from .run_with import run_with
+from .ui_run import run
+from .ui_run_with import run_with

+ 169 - 0
nicegui/ui_run.py

@@ -0,0 +1,169 @@
+import multiprocessing
+import os
+import sys
+from pathlib import Path
+from typing import Any, List, Literal, Optional, Tuple, Union
+
+import __main__
+from starlette.routing import Route
+from uvicorn.main import STARTUP_FAILURE
+from uvicorn.supervisors import ChangeReload, Multiprocess
+
+from . import air, globals, helpers  # pylint: disable=redefined-builtin
+from . import native as native_module
+from .client import Client
+from .language import Language
+from .logging import log
+from .server import CustomServerConfig, Server
+
+APP_IMPORT_STRING = 'nicegui:app'
+
+
+def run(*,
+        host: Optional[str] = None,
+        port: int = 8080,
+        title: str = 'NiceGUI',
+        viewport: str = 'width=device-width, initial-scale=1',
+        favicon: Optional[Union[str, Path]] = None,
+        dark: Optional[bool] = False,
+        language: Language = 'en-US',
+        binding_refresh_interval: float = 0.1,
+        reconnect_timeout: float = 3.0,
+        show: bool = True,
+        on_air: Optional[Union[str, Literal[True]]] = None,
+        native: bool = False,
+        window_size: Optional[Tuple[int, int]] = None,
+        fullscreen: bool = False,
+        frameless: bool = False,
+        reload: bool = True,
+        uvicorn_logging_level: str = 'warning',
+        uvicorn_reload_dirs: str = '.',
+        uvicorn_reload_includes: str = '*.py',
+        uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
+        tailwind: bool = True,
+        prod_js: bool = True,
+        endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
+        storage_secret: Optional[str] = None,
+        **kwargs: Any,
+        ) -> None:
+    '''ui.run
+
+    You can call `ui.run()` with optional arguments:
+
+    :param host: start server with this host (defaults to `'127.0.0.1` in native mode, otherwise `'0.0.0.0'`)
+    :param port: use this port (default: `8080`)
+    :param title: page title (default: `'NiceGUI'`, can be overwritten per page)
+    :param viewport: page meta viewport content (default: `'width=device-width, initial-scale=1'`, can be overwritten per page)
+    :param favicon: relative filepath, absolute URL to a favicon (default: `None`, NiceGUI icon will be used) or emoji (e.g. `'🚀'`, works for most browsers)
+    :param dark: whether to use Quasar's dark mode (default: `False`, use `None` for "auto" mode)
+    :param language: language for Quasar elements (default: `'en-US'`)
+    :param binding_refresh_interval: time between binding updates (default: `0.1` seconds, bigger is more CPU friendly)
+    :param reconnect_timeout: maximum time the server waits for the browser to reconnect (default: 3.0 seconds)
+    :param show: automatically open the UI in a browser tab (default: `True`)
+    :param on_air: tech preview: `allows temporary remote access <https://nicegui.io/documentation#nicegui_on_air>`_ if set to `True` (default: disabled)
+    :param native: open the UI in a native window of size 800x600 (default: `False`, deactivates `show`, automatically finds an open port)
+    :param window_size: open the UI in a native window with the provided size (e.g. `(1024, 786)`, default: `None`, also activates `native`)
+    :param fullscreen: open the UI in a fullscreen window (default: `False`, also activates `native`)
+    :param frameless: open the UI in a frameless window (default: `False`, also activates `native`)
+    :param reload: automatically reload the UI on file changes (default: `True`)
+    :param uvicorn_logging_level: logging level for uvicorn server (default: `'warning'`)
+    :param uvicorn_reload_dirs: string with comma-separated list for directories to be monitored (default is current working directory only)
+    :param uvicorn_reload_includes: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'.py'`)
+    :param uvicorn_reload_excludes: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
+    :param tailwind: whether to use Tailwind (experimental, default: `True`)
+    :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
+    :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
+    :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
+    :param kwargs: additional keyword arguments are passed to `uvicorn.run`    
+    '''
+    globals.ui_run_has_been_called = True
+    globals.reload = reload
+    globals.title = title
+    globals.viewport = viewport
+    globals.favicon = favicon
+    globals.dark = dark
+    globals.language = language
+    globals.binding_refresh_interval = binding_refresh_interval
+    globals.reconnect_timeout = reconnect_timeout
+    globals.tailwind = tailwind
+    globals.prod_js = prod_js
+    globals.endpoint_documentation = endpoint_documentation
+
+    for route in globals.app.routes:
+        if not isinstance(route, Route):
+            continue
+        if route.path.startswith('/_nicegui') and hasattr(route, 'methods'):
+            route.include_in_schema = endpoint_documentation in {'internal', 'all'}
+        if route.path == '/' or route.path in Client.page_routes.values():
+            route.include_in_schema = endpoint_documentation in {'page', 'all'}
+
+    if on_air:
+        air.instance = air.Air('' if on_air is True else on_air)
+
+    if multiprocessing.current_process().name != 'MainProcess':
+        return
+
+    if reload and not hasattr(__main__, '__file__'):
+        log.warning('auto-reloading is only supported when running from a file')
+        globals.reload = reload = False
+
+    if fullscreen:
+        native = True
+    if frameless:
+        native = True
+    if window_size:
+        native = True
+    if native:
+        show = False
+        host = host or '127.0.0.1'
+        port = native_module.find_open_port()
+        width, height = window_size or (800, 600)
+        native_module.activate(host, port, title, width, height, fullscreen, frameless)
+    else:
+        host = host or '0.0.0.0'
+
+    # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
+    os.environ['NICEGUI_HOST'] = host
+    os.environ['NICEGUI_PORT'] = str(port)
+
+    if show:
+        helpers.schedule_browser(host, port)
+
+    def split_args(args: str) -> List[str]:
+        return [a.strip() for a in args.split(',')]
+
+    # NOTE: The following lines are basically a copy of `uvicorn.run`, but keep a reference to the `server`.
+
+    config = CustomServerConfig(
+        APP_IMPORT_STRING if reload else globals.app,
+        host=host,
+        port=port,
+        reload=reload,
+        reload_includes=split_args(uvicorn_reload_includes) if reload else None,
+        reload_excludes=split_args(uvicorn_reload_excludes) if reload else None,
+        reload_dirs=split_args(uvicorn_reload_dirs) if reload else None,
+        log_level=uvicorn_logging_level,
+        **kwargs,
+    )
+    config.storage_secret = storage_secret
+    config.method_queue = native_module.method_queue if native else None
+    config.response_queue = native_module.response_queue if native else None
+    Server.create_singleton(config)
+
+    if (reload or config.workers > 1) and not isinstance(config.app, str):
+        log.warning('You must pass the application as an import string to enable "reload" or "workers".')
+        sys.exit(1)
+
+    if config.should_reload:
+        sock = config.bind_socket()
+        ChangeReload(config, target=Server.instance.run, sockets=[sock]).run()
+    elif config.workers > 1:
+        sock = config.bind_socket()
+        Multiprocess(config, target=Server.instance.run, sockets=[sock]).run()
+    else:
+        Server.instance.run()
+    if config.uds:
+        os.remove(config.uds)  # pragma: py-win32
+
+    if not Server.instance.started and not config.should_reload and config.workers == 1:
+        sys.exit(STARTUP_FAILURE)

+ 0 - 1
nicegui/run_with.py → nicegui/ui_run_with.py

@@ -4,7 +4,6 @@ from typing import Optional, Union
 from fastapi import FastAPI
 from fastapi import FastAPI
 
 
 from . import globals, storage  # pylint: disable=redefined-builtin
 from . import globals, storage  # pylint: disable=redefined-builtin
-from .app import App
 from .language import Language
 from .language import Language
 from .nicegui import handle_shutdown, handle_startup
 from .nicegui import handle_shutdown, handle_startup
 
 

+ 2 - 3
nicegui/welcome.py

@@ -3,8 +3,7 @@ import socket
 from typing import List
 from typing import List
 
 
 from . import globals  # pylint: disable=redefined-builtin
 from . import globals  # pylint: disable=redefined-builtin
-from . import optional_features
-from .run_executor import io_bound
+from . import optional_features, run
 
 
 try:
 try:
     import netifaces
     import netifaces
@@ -35,7 +34,7 @@ async def print_message() -> None:
     print('NiceGUI ready to go ', end='', flush=True)
     print('NiceGUI ready to go ', end='', flush=True)
     host = os.environ['NICEGUI_HOST']
     host = os.environ['NICEGUI_HOST']
     port = os.environ['NICEGUI_PORT']
     port = os.environ['NICEGUI_PORT']
-    ips = set((await io_bound(_get_all_ips)) if host == '0.0.0.0' else [])
+    ips = set((await run.io_bound(_get_all_ips)) if host == '0.0.0.0' else [])
     ips.discard('127.0.0.1')
     ips.discard('127.0.0.1')
     urls = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
     urls = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
     globals.app.urls.update(urls)
     globals.app.urls.update(urls)