Jelajahi Sumber

remove pre-evaluation of ui.run

Falko Schindler 2 tahun lalu
induk
melakukan
6facf8b6c8
7 mengubah file dengan 104 tambahan dan 163 penghapusan
  1. 2 2
      nicegui/binding.py
  2. 0 60
      nicegui/config.py
  3. 48 25
      nicegui/elements/page.py
  4. 3 3
      nicegui/globals.py
  5. 3 6
      nicegui/nicegui.py
  6. 45 64
      nicegui/run.py
  7. 3 3
      nicegui/ui.py

+ 2 - 2
nicegui/binding.py

@@ -6,7 +6,7 @@ from typing import Any, Callable, Optional, Set, Tuple
 
 from justpy.htmlcomponents import HTMLBaseComponent
 
-from .globals import config
+from . import globals
 from .task_logger import create_task
 
 bindings = defaultdict(list)
@@ -31,7 +31,7 @@ async def loop():
         update_views(visited_views)
         if time.time() - t > 0.01:
             logging.warning(f'binding update for {len(visited_views)} visited views took {time.time() - t:.3f} s')
-        await asyncio.sleep(config.binding_refresh_interval)
+        await asyncio.sleep(globals.config.binding_refresh_interval)
 
 
 async def update_views_async(views: Set[HTMLBaseComponent]):

+ 0 - 60
nicegui/config.py

@@ -1,11 +1,7 @@
-import ast
-import inspect
 import os
 from dataclasses import dataclass
 from typing import Optional
 
-from . import globals
-
 
 @dataclass
 class Config():
@@ -15,61 +11,5 @@ class Config():
     title: str = 'NiceGUI'
     favicon: str = 'favicon.ico'
     dark: Optional[bool] = False
-    reload: bool = True
-    show: bool = True
-    uvicorn_logging_level: str = 'warning'
-    uvicorn_reload_dirs: str = '.'
-    uvicorn_reload_includes: str = '*.py'
-    uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*'
     main_page_classes: str = 'q-ma-md column items-start'
     binding_refresh_interval: float = 0.1
-    exclude: str = ''
-
-
-excluded_endings = (
-    '<string>',
-    'spawn.py',
-    'runpy.py',
-    os.path.join('debugpy', 'server', 'cli.py'),
-    os.path.join('debugpy', '__main__.py'),
-    'pydevd.py',
-    '_pydev_execfile.py',
-)
-for f in reversed(inspect.stack()):
-    if not any(f.filename.endswith(ending) for ending in excluded_endings):
-        filepath = f.filename
-        break
-else:
-    raise Exception('Could not find main script in stacktrace')
-
-try:
-    with open(filepath) as f:
-        source = f.read()
-except FileNotFoundError:
-    config = Config()
-else:
-    for node in ast.walk(ast.parse(source)):
-        try:
-            func = node.value.func
-            if func.value.id == 'ui' and func.attr == 'run':
-                args = {
-                    keyword.arg:
-                        keyword.value.n if isinstance(keyword.value, ast.Num) else
-                        keyword.value.s if isinstance(keyword.value, ast.Str) else
-                        keyword.value.value
-                    for keyword in node.value.keywords
-                }
-                config = Config(**args)
-                globals.pre_evaluation_succeeded = True
-                break
-        except AttributeError:
-            continue
-    else:
-        config = Config()
-
-os.environ['HOST'] = config.host
-os.environ['PORT'] = str(config.port)
-os.environ['STATIC_DIRECTORY'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static')
-os.environ['TEMPLATES_DIRECTORY'] = os.path.join(os.environ['STATIC_DIRECTORY'], 'templates')
-
-globals.config = config

+ 48 - 25
nicegui/elements/page.py

@@ -13,7 +13,7 @@ from addict import Dict
 from pygments.formatters import HtmlFormatter
 from starlette.requests import Request
 
-from ..globals import config, connect_handlers, disconnect_handlers, page_builders, view_stack
+from .. import globals
 from ..helpers import is_coroutine
 
 
@@ -36,7 +36,6 @@ class PageBuilder:
 class Page(jp.QuasarPage):
 
     def __init__(self,
-                 route: str,
                  title: Optional[str] = None,
                  *,
                  favicon: Optional[str] = None,
@@ -50,10 +49,14 @@ class Page(jp.QuasarPage):
                  ):
         super().__init__()
 
-        self.route = route
-        self.title = title or config.title
-        self.favicon = favicon or config.favicon
-        self.dark = dark if dark is not ... else config.dark
+        if globals.config:
+            self.title = title or globals.config.title
+            self.favicon = favicon or globals.config.favicon
+            self.dark = dark if dark is not ... else globals.config.dark
+        else:
+            self.title = title
+            self.favicon = favicon
+            self.dark = dark if dark is not ... else None
         self.tailwind = True  # use Tailwind classes instead of Quasars
         self.css = css
         self.connect_handler = on_connect
@@ -69,13 +72,13 @@ class Page(jp.QuasarPage):
         self.view.add_page(self)
 
     async def _route_function(self, request: Request):
-        for connect_handler in connect_handlers + ([self.connect_handler] if self.connect_handler else []):
-            arg_count = len(inspect.signature(connect_handler).parameters)
-            is_coro = is_coroutine(connect_handler)
+        for handler in globals.connect_handlers + ([self.connect_handler] if self.connect_handler else []):
+            arg_count = len(inspect.signature(handler).parameters)
+            is_coro = is_coroutine(handler)
             if arg_count == 1:
-                await connect_handler(request) if is_coro else connect_handler(request)
+                await handler(request) if is_coro else handler(request)
             elif arg_count == 0:
-                await connect_handler() if is_coro else connect_handler()
+                await handler() if is_coro else handler()
             else:
                 raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
         return self
@@ -93,13 +96,13 @@ class Page(jp.QuasarPage):
         return False
 
     async def on_disconnect(self, websocket=None) -> None:
-        for disconnect_handler in ([self.disconnect_handler] if self.disconnect_handler else []) + disconnect_handlers:
-            arg_count = len(inspect.signature(disconnect_handler).parameters)
-            is_coro = is_coroutine(disconnect_handler)
+        for handler in globals.disconnect_handlers + ([self.disconnect_handler] if self.disconnect_handler else[]):
+            arg_count = len(inspect.signature(handler).parameters)
+            is_coro = is_coroutine(handler)
             if arg_count == 1:
-                await disconnect_handler(websocket) if is_coro else disconnect_handler(websocket)
+                await handler(websocket) if is_coro else handler(websocket)
             elif arg_count == 0:
-                await disconnect_handler() if is_coro else disconnect_handler()
+                await handler() if is_coro else handler()
             else:
                 raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
         await super().on_disconnect(websocket)
@@ -171,7 +174,6 @@ def page(self,
         @wraps(func)
         async def decorated():
             page = Page(
-                route=route,
                 title=title,
                 favicon=favicon,
                 dark=dark,
@@ -182,28 +184,49 @@ def page(self,
                 on_disconnect=on_disconnect,
                 shared=shared,
             )
-            view_stack.append(page.view)
+            globals.view_stack.append(page.view)
             await func() if is_coroutine(func) else func()
-            view_stack.pop()
+            globals.view_stack.pop()
             return page
-        page_builders[route] = PageBuilder(decorated, shared)
+        globals.page_builders[route] = PageBuilder(decorated, shared)
         return decorated
     return decorator
 
 
 def get_current_view() -> jp.HTMLBaseComponent:
-    if not view_stack:
-        page = Page(route='/', title=config.title, dark=config.dark, classes=config.main_page_classes, shared=True)
-        view_stack.append(page.view)
+    if not globals.view_stack:
+        page = Page(shared=True)
+        globals.view_stack.append(page.view)
+        globals.has_auto_index_page = True  # NOTE: this automatically created page will get some attributes at startup
         jp.Route('/', page._route_function)
-    return view_stack[-1]
+    return globals.view_stack[-1]
 
 
 def error404() -> jp.QuasarPage:
-    wp = jp.QuasarPage(title=config.title, favicon=config.favicon, dark=config.dark, tailwind=True)
+    wp = jp.QuasarPage(title=globals.config.title, favicon=globals.config.favicon,
+                       dark=globals.config.dark, tailwind=True)
     div = jp.Div(a=wp, classes='py-20 text-center')
     jp.Div(a=div, classes='text-8xl py-5', text='☹',
            style='font-family: "Arial Unicode MS", "Times New Roman", Times, serif;')
     jp.Div(a=div, classes='text-6xl py-5', text='404')
     jp.Div(a=div, classes='text-xl py-5', text='This page doesn\'t exist.')
     return wp
+
+
+def init_auto_index_page() -> None:
+    if not globals.has_auto_index_page:
+        return
+    page: Page = get_current_view().pages[0]
+    page.title = globals.config.title
+    page.favicon = globals.config.favicon
+    page.dark = globals.config.dark
+    page.view.classes = globals.config.main_page_classes
+
+
+async def create_page_routes() -> None:
+    jp.Route("/{path:path}", error404, last=True)
+
+    for route, page_builder in globals.page_builders.items():
+        if page_builder.shared:
+            await page_builder.build()
+        jp.Route(route, page_builder.route_function)

+ 3 - 3
nicegui/globals.py

@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import asyncio
 import logging
-from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Union
+from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional, Union
 
 from uvicorn import Server
 
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
     from .elements.page import PageBuilder
 
 app: 'Starlette'
-config: 'Config'
+config: Optional['Config'] = None
 server: Server
 page_builders: Dict[str, 'PageBuilder'] = {}
 view_stack: List['jp.HTMLBaseComponent'] = []
@@ -24,7 +24,7 @@ connect_handlers: List[Union[Callable, Awaitable]] = []
 disconnect_handlers: List[Union[Callable, Awaitable]] = []
 startup_handlers: List[Union[Callable, Awaitable]] = []
 shutdown_handlers: List[Union[Callable, Awaitable]] = []
-pre_evaluation_succeeded: bool = False
+has_auto_index_page: bool = False
 
 
 def find_route(function: Callable) -> str:

+ 3 - 6
nicegui/nicegui.py

@@ -10,9 +10,9 @@ if True:  # NOTE: prevent formatter from mixing up these lines
     builtins.print = print_backup
 
 from . import binding, globals
+from .elements.page import create_page_routes, init_auto_index_page
 from .task_logger import create_task
 from .timer import Timer
-from .elements.page import error404
 
 jp.app.router.on_startup.clear()  # NOTE: remove JustPy's original startup function
 
@@ -27,11 +27,8 @@ async def patched_justpy_startup():
 
 @jp.app.on_event('startup')
 async def startup():
-    jp.Route("/{path:path}", error404, last=True)
-    for route, page_builder in globals.page_builders.items():
-        if page_builder.shared:
-            await page_builder.build()
-        jp.Route(route, page_builder.route_function)
+    init_auto_index_page()
+    await create_page_routes()
     globals.tasks.extend(create_task(t.coro, name=t.name) for t in Timer.prepared_coroutines)
     Timer.prepared_coroutines.clear()
     globals.tasks.extend(create_task(t, name='startup task')

+ 45 - 64
nicegui/run.py

@@ -13,27 +13,60 @@ from . import globals
 from .config import Config
 
 
-def _start_server(config: Config) -> None:
-    if config.show:
-        webbrowser.open(f'http://{config.host if config.host != "0.0.0.0" else "127.0.0.1"}:{config.port}/')
+def run(self, *,
+        host: str = os.environ.get('HOST', '0.0.0.0'),
+        port: int = int(os.environ.get('PORT', '8080')),
+        title: str = 'NiceGUI',
+        favicon: str = 'favicon.ico',
+        dark: Optional[bool] = False,
+        reload: bool = True,
+        show: bool = True,
+        uvicorn_logging_level: str = 'warning',
+        uvicorn_reload_dirs: str = '.',
+        uvicorn_reload_includes: str = '*.py',
+        uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
+        main_page_classes: str = 'q-ma-md column items-start',
+        binding_refresh_interval: float = 0.1,
+        ):
+    globals.config = Config(
+        host=host,
+        port=port,
+        title=title,
+        favicon=favicon,
+        dark=dark,
+        main_page_classes=main_page_classes,
+        binding_refresh_interval=binding_refresh_interval,
+    )
+    os.environ['HOST'] = globals.config.host
+    os.environ['PORT'] = str(globals.config.port)
+    os.environ['STATIC_DIRECTORY'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static')
+    os.environ['TEMPLATES_DIRECTORY'] = os.path.join(os.environ['STATIC_DIRECTORY'], 'templates')
+
+    if inspect.stack()[-2].filename.endswith('spawn.py'):
+        return
+
+    if show:
+        webbrowser.open(f'http://{host if host != "0.0.0.0" else "127.0.0.1"}:{port}/')
 
     def split_args(args: str) -> List[str]:
         return args.split(',') if ',' in args else [args]
 
+    # NOTE: The following lines are basically a copy of `uvicorn.run`, but keep a reference to the `server`.
+
     config = uvicorn.Config(
-        'nicegui:app' if config.reload else globals.app,
-        host=config.host,
-        port=config.port,
+        'nicegui:app' if reload else globals.app,
+        host=host,
+        port=port,
         lifespan='on',
-        reload=config.reload,
-        reload_includes=split_args(config.uvicorn_reload_includes) if config.reload else None,
-        reload_excludes=split_args(config.uvicorn_reload_excludes) if config.reload else None,
-        reload_dirs=split_args(config.uvicorn_reload_dirs) if config.reload else None,
-        log_level=config.uvicorn_logging_level,
+        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,
     )
     globals.server = uvicorn.Server(config=config)
 
-    if (config.reload or config.workers > 1) and not isinstance(config.app, str):
+    if (reload or config.workers > 1) and not isinstance(config.app, str):
         logging.warning('You must pass the application as an import string to enable "reload" or "workers".')
         sys.exit(1)
 
@@ -50,55 +83,3 @@ def _start_server(config: Config) -> None:
 
     if not globals.server.started and not config.should_reload and config.workers == 1:
         sys.exit(STARTUP_FAILURE)
-
-
-if globals.pre_evaluation_succeeded and globals.config.reload and not inspect.stack()[-2].filename.endswith('spawn.py'):
-    _start_server(globals.config)
-    sys.exit()
-
-
-def run(self, *,
-        host: str = os.environ.get('HOST', '0.0.0.0'),
-        port: int = int(os.environ.get('PORT', '8080')),
-        title: str = 'NiceGUI',
-        favicon: str = 'favicon.ico',
-        dark: Optional[bool] = False,
-        reload: bool = True,
-        show: bool = True,
-        uvicorn_logging_level: str = 'warning',
-        uvicorn_reload_dirs: str = '.',
-        uvicorn_reload_includes: str = '*.py',
-        uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
-        main_page_classes: str = 'q-ma-md column items-start',
-        binding_refresh_interval: float = 0.1,
-        exclude: str = '',
-        ):
-    if globals.pre_evaluation_succeeded and globals.config.reload == True:
-        return  # server has already started after pre-evaluating ui.run()
-
-    globals.config.host = host
-    globals.config.port = port
-    globals.config.title = title
-    globals.config.favicon = favicon
-    globals.config.dark = dark
-    globals.config.reload = reload
-    globals.config.show = show
-    globals.config.uvicorn_logging_level = uvicorn_logging_level
-    globals.config.uvicorn_reload_dirs = uvicorn_reload_dirs
-    globals.config.uvicorn_reload_includes = uvicorn_reload_includes
-    globals.config.uvicorn_reload_excludes = uvicorn_reload_excludes
-    globals.config.main_page_classes = main_page_classes
-    globals.config.binding_refresh_interval = binding_refresh_interval
-
-    if inspect.stack()[-2].filename.endswith('spawn.py'):
-        return  # server is reloading
-
-    if not globals.pre_evaluation_succeeded:
-        if exclude or reload:
-            logging.warning('Failed to pre-evaluate ui.run().')
-        if exclude:
-            logging.warning('The `exclude` argument will be ignored.')
-        if reload:
-            logging.warning('Reloading main script...')
-
-    _start_server(globals.config)

+ 3 - 3
nicegui/ui.py

@@ -3,11 +3,11 @@ import os
 
 
 class Ui:
-    from .config import config  # NOTE: before run
     from .run import run  # NOTE: before justpy
 
-    _excludes = [word.strip().lower() for word in config.exclude.split(',')]
-    _excludes = [e[:-3] if e.endswith('.js') else e for e in _excludes]  # NOTE: for python <3.9 without removesuffix
+    # _excludes = [word.strip().lower() for word in globals.config.exclude.split(',')]
+    # _excludes = [e[:-3] if e.endswith('.js') else e for e in _excludes]  # NOTE: for python <3.9 without removesuffix
+    _excludes = []
     os.environ['HIGHCHARTS'] = str('highcharts' not in _excludes)
     os.environ['AGGRID'] = str('aggrid' not in _excludes)