Ver código fonte

extract app config; remove "globals" from public API and rename it to "core"

Falko Schindler 1 ano atrás
pai
commit
2b130fb5cb

+ 3 - 5
main.py

@@ -14,9 +14,7 @@ from starlette.middleware.sessions import SessionMiddleware
 from starlette.types import ASGIApp, Receive, Scope, Send
 
 import prometheus
-from nicegui import Client, app
-from nicegui import globals as nicegui_globals
-from nicegui import ui
+from nicegui import Client, app, ui
 from website import documentation, example_card, svg
 from website.demo import bash_window, browser_window, python_window
 from website.documentation_tools import create_anchor_name, element_demo, generate_class_doc
@@ -67,8 +65,8 @@ async def redirect_reference_to_documentation(request: Request,
 
 # NOTE In our global fly.io deployment we need to make sure that we connect back to the same instance.
 fly_instance_id = os.environ.get('FLY_ALLOC_ID', 'local').split('-')[0]
-nicegui_globals.socket_io_js_extra_headers['fly-force-instance-id'] = fly_instance_id  # for HTTP long polling
-nicegui_globals.socket_io_js_query_params['fly_instance_id'] = fly_instance_id  # for websocket (FlyReplayMiddleware)
+app.extra_config.socket_io_js_extra_headers['fly-force-instance-id'] = fly_instance_id  # for HTTP long polling
+app.extra_config.socket_io_js_query_params['fly_instance_id'] = fly_instance_id  # for websocket (FlyReplayMiddleware)
 
 
 class FlyReplayMiddleware(BaseHTTPMiddleware):

+ 0 - 1
nicegui/__init__.py

@@ -15,7 +15,6 @@ __all__ = [
     'Client',
     'context',
     'elements',
-    'globals',
     'log',
     'optional_features',
     'run',

+ 4 - 4
nicegui/air.py

@@ -7,7 +7,7 @@ import httpx
 import socketio
 import socketio.exceptions
 
-from . import background_tasks, globals  # pylint: disable=redefined-builtin
+from . import background_tasks, core
 from .client import Client
 from .logging import log
 
@@ -19,7 +19,7 @@ class Air:
     def __init__(self, token: str) -> None:
         self.token = token
         self.relay = socketio.AsyncClient()
-        self.client = httpx.AsyncClient(app=globals.app)
+        self.client = httpx.AsyncClient(app=core.app)
         self.connecting = False
 
         @self.relay.on('http')
@@ -54,7 +54,7 @@ class Air:
 
         @self.relay.on('ready')
         def _handle_ready(data: Dict[str, Any]) -> None:
-            globals.app.urls.add(data['device_url'])
+            core.app.urls.add(data['device_url'])
             print(f'NiceGUI is on air at {data["device_url"]}', flush=True)
 
         @self.relay.on('error')
@@ -147,7 +147,7 @@ class Air:
         """Whether the given target ID is an On Air client or a SocketIO room."""
         if target_id in Client.instances:
             return Client.instances[target_id].on_air
-        return target_id in globals.sio.manager.rooms
+        return target_id in core.sio.manager.rooms
 
 
 instance: Optional[Air] = None

+ 5 - 2
nicegui/app.py

@@ -7,7 +7,8 @@ from fastapi import FastAPI, HTTPException, Request
 from fastapi.responses import FileResponse, StreamingResponse
 from fastapi.staticfiles import StaticFiles
 
-from . import background_tasks, globals, helpers  # pylint: disable=redefined-builtin
+from . import background_tasks, core, helpers
+from .app_config import AppConfig, ExtraConfig
 from .client import Client
 from .logging import log
 from .native import NativeConfig
@@ -31,6 +32,8 @@ class App(FastAPI):
         self.storage = Storage()
         self.urls = ObservableSet()
         self._state: State = State.STOPPED
+        self.config: AppConfig
+        self.extra_config = ExtraConfig()
 
         self._startup_handlers: List[Union[Callable[..., Any], Awaitable]] = []
         self._shutdown_handlers: List[Union[Callable[..., Any], Awaitable]] = []
@@ -122,7 +125,7 @@ class App(FastAPI):
         This will programmatically stop the server.
         Only possible when auto-reload is disabled.
         """
-        if globals.reload:
+        if core.app.config.reload:
             raise RuntimeError('calling shutdown() is not supported when auto-reload is enabled')
         if self.native.main_window:
             self.native.main_window.destroy()

+ 39 - 0
nicegui/app_config.py

@@ -0,0 +1,39 @@
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Dict, List, Literal, Optional, Union
+
+from .dataclasses import KWONLY_SLOTS
+from .language import Language
+
+
+@dataclass(**KWONLY_SLOTS)
+class AppConfig:
+    reload: bool
+    title: str
+    viewport: str
+    favicon: Optional[Union[str, Path]]
+    dark: Optional[bool]
+    language: Language
+    binding_refresh_interval: float
+    reconnect_timeout: float
+    tailwind: bool
+    prod_js: bool
+
+
+@dataclass(**KWONLY_SLOTS)
+class ExtraConfig:
+    endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none'
+    socket_io_js_query_params: Dict = field(default_factory=dict)
+    socket_io_js_extra_headers: Dict = field(default_factory=dict)
+    socket_io_js_transports: List[Literal['websocket', 'polling']] = \
+        field(default_factory=lambda: ['websocket', 'polling'])  # NOTE: we favor websocket
+    quasar_config: Dict = \
+        field(default_factory=lambda: {
+            'brand': {
+                'primary': '#5898d4',
+            },
+            'loadingBar': {
+                'color': 'primary',
+                'skipHijack': False,
+            },
+        })

+ 4 - 8
nicegui/background_tasks.py

@@ -2,12 +2,9 @@
 from __future__ import annotations
 
 import asyncio
-import sys
 from typing import Awaitable, Dict, Set
 
-from . import globals  # pylint: disable=redefined-builtin,cyclic-import
-
-name_supported = sys.version_info[1] >= 8
+from . import core
 
 running_tasks: Set[asyncio.Task] = set()
 lazy_tasks_running: Dict[str, asyncio.Task] = {}
@@ -21,10 +18,9 @@ def create(coroutine: Awaitable, *, name: str = 'unnamed task') -> asyncio.Task:
     Also a reference to the task is kept until it is done, so that the task is not garbage collected mid-execution.
     See https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task.
     """
-    assert globals.loop is not None
+    assert core.loop is not None
     assert asyncio.iscoroutine(coroutine)
-    task: asyncio.Task = \
-        globals.loop.create_task(coroutine, name=name) if name_supported else globals.loop.create_task(coroutine)
+    task: asyncio.Task = core.loop.create_task(coroutine, name=name)
     task.add_done_callback(_handle_task_result)
     running_tasks.add(task)
     task.add_done_callback(running_tasks.discard)
@@ -57,4 +53,4 @@ def _handle_task_result(task: asyncio.Task) -> None:
     except asyncio.CancelledError:
         pass
     except Exception as e:
-        globals.app.handle_exception(e)
+        core.app.handle_exception(e)

+ 2 - 2
nicegui/binding.py

@@ -4,7 +4,7 @@ from collections import defaultdict
 from collections.abc import Mapping
 from typing import Any, Callable, DefaultDict, Dict, Iterable, List, Optional, Set, Tuple, Type, Union
 
-from . import globals  # pylint: disable=redefined-builtin
+from . import core
 from .logging import log
 
 MAX_PROPAGATION_TIME = 0.01
@@ -37,7 +37,7 @@ async def refresh_loop() -> None:
     """Refresh all bindings in an endless loop."""
     while True:
         _refresh_step()
-        await asyncio.sleep(globals.binding_refresh_interval)
+        await asyncio.sleep(core.app.config.binding_refresh_interval)
 
 
 def _refresh_step() -> None:

+ 10 - 10
nicegui/client.py

@@ -13,7 +13,7 @@ from fastapi.templating import Jinja2Templates
 
 from nicegui import json
 
-from . import background_tasks, binding, globals, outbox  # pylint: disable=redefined-builtin
+from . import background_tasks, binding, core, outbox
 from .awaitable_response import AwaitableResponse
 from .dependencies import generate_resources
 from .element import Element
@@ -97,7 +97,7 @@ class Client:
         elements = json.dumps({
             id: element._to_dict() for id, element in self.elements.items()  # pylint: disable=protected-access
         })
-        socket_io_js_query_params = {**globals.socket_io_js_query_params, 'client_id': self.id}
+        socket_io_js_query_params = {**core.app.extra_config.socket_io_js_query_params, 'client_id': self.id}
         vue_html, vue_styles, vue_scripts, imports, js_imports = generate_resources(prefix, self.elements.values())
         return templates.TemplateResponse('index.html', {
             'request': request,
@@ -111,18 +111,18 @@ class Client:
             'vue_scripts': '\n'.join(vue_scripts),
             'imports': json.dumps(imports),
             'js_imports': '\n'.join(js_imports),
-            'quasar_config': json.dumps(globals.quasar_config),
+            'quasar_config': json.dumps(core.app.extra_config.quasar_config),
             'title': self.page.resolve_title(),
             'viewport': self.page.resolve_viewport(),
             'favicon_url': get_favicon_url(self.page, prefix),
             'dark': str(self.page.resolve_dark()),
             'language': self.page.resolve_language(),
             'prefix': prefix,
-            'tailwind': globals.tailwind,
-            'prod_js': globals.prod_js,
+            'tailwind': core.app.config.tailwind,
+            'prod_js': core.app.config.prod_js,
             'socket_io_js_query_params': socket_io_js_query_params,
-            'socket_io_js_extra_headers': globals.socket_io_js_extra_headers,
-            'socket_io_js_transports': globals.socket_io_js_transports,
+            'socket_io_js_extra_headers': core.app.extra_config.socket_io_js_extra_headers,
+            'socket_io_js_transports': core.app.extra_config.socket_io_js_transports,
         }, status_code, {'Cache-Control': 'no-store', 'X-NiceGUI-Content': 'page'})
 
     async def connected(self, timeout: float = 3.0, check_interval: float = 0.1) -> None:
@@ -202,19 +202,19 @@ class Client:
             self.disconnect_task = None
         for t in self.connect_handlers:
             safe_invoke(t, self)
-        for t in globals.app._connect_handlers:  # pylint: disable=protected-access
+        for t in core.app._connect_handlers:  # pylint: disable=protected-access
             safe_invoke(t, self)
 
     def handle_disconnect(self) -> None:
         """Wait for the browser to reconnect; invoke disconnect handlers if it doesn't."""
         async def handle_disconnect() -> None:
-            delay = self.page.reconnect_timeout if self.page.reconnect_timeout is not None else globals.reconnect_timeout
+            delay = self.page.reconnect_timeout if self.page.reconnect_timeout is not None else core.app.config.reconnect_timeout
             await asyncio.sleep(delay)
             if not self.shared:
                 self.delete()
             for t in self.disconnect_handlers:
                 safe_invoke(t, self)
-            for t in globals.app._disconnect_handlers:  # pylint: disable=protected-access
+            for t in core.app._disconnect_handlers:  # pylint: disable=protected-access
                 safe_invoke(t, self)
         self.disconnect_task = background_tasks.create(handle_disconnect())
 

+ 13 - 0
nicegui/core.py

@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+import asyncio
+from typing import TYPE_CHECKING, Optional
+
+from socketio import AsyncServer
+
+if TYPE_CHECKING:
+    from .app import App
+
+app: App
+sio: AsyncServer
+loop: Optional[asyncio.AbstractEventLoop] = None

+ 2 - 2
nicegui/element.py

@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional,
 
 from typing_extensions import Self
 
-from . import context, events, globals, json, outbox, storage  # pylint: disable=redefined-builtin
+from . import context, core, events, json, outbox, storage
 from .awaitable_response import AwaitableResponse
 from .dependencies import Component, Library, register_library, register_vue_component
 from .elements.mixins.visibility import Visibility
@@ -409,7 +409,7 @@ class Element(Visibility):
         :param name: name of the method
         :param args: arguments to pass to the method
         """
-        if not globals.loop:
+        if not core.loop:
             return AwaitableResponse(None, None)
         return self.client.run_javascript(f'return runMethod({self.id}, "{name}", {json.dumps(args)})')
 

+ 2 - 2
nicegui/elements/audio.py

@@ -1,7 +1,7 @@
 from pathlib import Path
 from typing import Union
 
-from .. import globals  # pylint: disable=redefined-builtin
+from .. import core
 from ..element import Element
 
 
@@ -28,7 +28,7 @@ class Audio(Element, component='audio.js'):
         """
         super().__init__()
         if Path(src).is_file():
-            src = globals.app.add_media_file(local_file=src)
+            src = core.app.add_media_file(local_file=src)
         self._props['src'] = src
         self._props['controls'] = controls
         self._props['autoplay'] = autoplay

+ 2 - 2
nicegui/elements/mixins/source_element.py

@@ -3,7 +3,7 @@ from typing import Any, Callable, Union
 
 from typing_extensions import Self, cast
 
-from ... import globals  # pylint: disable=redefined-builtin
+from ... import core
 from ...binding import BindableProperty, bind, bind_from, bind_to
 from ...element import Element
 from ...helpers import is_file
@@ -16,7 +16,7 @@ class SourceElement(Element):
     def __init__(self, *, source: Union[str, Path], **kwargs: Any) -> None:
         super().__init__(**kwargs)
         if is_file(source):
-            source = globals.app.add_static_file(local_file=source)
+            source = core.app.add_static_file(local_file=source)
         self.source = source
         self._props['src'] = source
 

+ 7 - 7
nicegui/elements/timer.py

@@ -3,7 +3,7 @@ import time
 from contextlib import nullcontext
 from typing import Any, Callable, Optional
 
-from .. import background_tasks, globals, helpers  # pylint: disable=redefined-builtin
+from .. import background_tasks, core, helpers
 from ..binding import BindableProperty
 from ..client import Client
 from ..element import Element
@@ -38,10 +38,10 @@ class Timer(Element, component='timer.js'):
         self._is_canceled: bool = False
 
         coroutine = self._run_once if once else self._run_in_loop
-        if globals.app.is_started:
+        if core.app.is_started:
             background_tasks.create(coroutine(), name=str(callback))
         else:
-            globals.app.on_startup(coroutine)
+            core.app.on_startup(coroutine)
 
     def activate(self) -> None:
         """Activate the timer."""
@@ -82,7 +82,7 @@ class Timer(Element, component='timer.js'):
                     except asyncio.CancelledError:
                         break
                     except Exception as e:
-                        globals.app.handle_exception(e)
+                        core.app.handle_exception(e)
                         await asyncio.sleep(self.interval)
         finally:
             self._cleanup()
@@ -94,7 +94,7 @@ class Timer(Element, component='timer.js'):
             if helpers.is_coroutine_function(self.callback):
                 await result
         except Exception as e:
-            globals.app.handle_exception(e)
+            core.app.handle_exception(e)
 
     async def _connected(self, timeout: float = 60.0) -> bool:
         """Wait for the client connection before the timer callback can be allowed to manipulate the state.
@@ -118,8 +118,8 @@ class Timer(Element, component='timer.js'):
             self.is_deleted or
             self.client.id not in Client.instances or
             self._is_canceled or
-            globals.app.is_stopping or
-            globals.app.is_stopped
+            core.app.is_stopping or
+            core.app.is_stopped
         )
 
     def _cleanup(self) -> None:

+ 2 - 2
nicegui/elements/video.py

@@ -1,7 +1,7 @@
 from pathlib import Path
 from typing import Union
 
-from .. import globals  # pylint: disable=redefined-builtin
+from .. import core
 from ..element import Element
 
 
@@ -28,7 +28,7 @@ class Video(Element, component='video.js'):
         """
         super().__init__()
         if Path(src).is_file():
-            src = globals.app.add_media_file(local_file=src)
+            src = core.app.add_media_file(local_file=src)
         self._props['src'] = src
         self._props['controls'] = controls
         self._props['autoplay'] = autoplay

+ 5 - 5
nicegui/events.py

@@ -5,7 +5,7 @@ from dataclasses import dataclass
 from inspect import Parameter, signature
 from typing import TYPE_CHECKING, Any, Awaitable, BinaryIO, Callable, Dict, List, Literal, Optional, Union
 
-from . import background_tasks, globals  # pylint: disable=redefined-builtin
+from . import background_tasks, core
 from .awaitable_response import AwaitableResponse
 from .dataclasses import KWONLY_SLOTS
 from .slot import Slot
@@ -440,10 +440,10 @@ def handle_event(handler: Optional[Callable[..., Any]], arguments: EventArgument
                     try:
                         await result
                     except Exception as e:
-                        globals.app.handle_exception(e)
-            if globals.loop and globals.loop.is_running():
+                        core.app.handle_exception(e)
+            if core.loop and core.loop.is_running():
                 background_tasks.create(wait_for_result(), name=str(handler))
             else:
-                globals.app.on_startup(wait_for_result())
+                core.app.on_startup(wait_for_result())
     except Exception as e:
-        globals.app.handle_exception(e)
+        core.app.handle_exception(e)

+ 7 - 7
nicegui/favicon.py

@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Optional, Tuple, Union
 
 from fastapi.responses import FileResponse, Response, StreamingResponse
 
-from . import globals  # pylint: disable=redefined-builtin
+from . import core
 from .helpers import is_file
 from .version import __version__
 
@@ -19,13 +19,13 @@ if TYPE_CHECKING:
 def create_favicon_route(path: str, favicon: Optional[Union[str, Path]]) -> None:
     """Create a favicon route for the given path."""
     if is_file(favicon):
-        globals.app.add_route('/favicon.ico' if path == '/' else f'{path}/favicon.ico',
-                              lambda _: FileResponse(favicon))  # type: ignore
+        core.app.add_route('/favicon.ico' if path == '/' else f'{path}/favicon.ico',
+                           lambda _: FileResponse(favicon))  # type: ignore
 
 
 def get_favicon_url(page: page, prefix: str) -> str:
     """Return the URL of the favicon for a given page."""
-    favicon = page.favicon or globals.favicon
+    favicon = page.favicon or core.app.config.favicon
     if not favicon:
         return f'{prefix}/_nicegui/{__version__}/static/favicon.ico'
 
@@ -46,9 +46,9 @@ def get_favicon_url(page: page, prefix: str) -> str:
 
 def get_favicon_response() -> Response:
     """Return the FastAPI response for the global favicon."""
-    if not globals.favicon:
-        raise ValueError(f'invalid favicon: {globals.favicon}')
-    favicon = str(globals.favicon).strip()
+    if not core.app.config.favicon:
+        raise ValueError(f'invalid favicon: {core.app.config.favicon}')
+    favicon = str(core.app.config.favicon).strip()
 
     if _is_svg(favicon):
         return Response(favicon, media_type='image/svg+xml')

+ 2 - 2
nicegui/functions/download.py

@@ -1,7 +1,7 @@
 from pathlib import Path
 from typing import Optional, Union
 
-from .. import context, globals, helpers  # pylint: disable=redefined-builtin
+from .. import context, core, helpers
 
 
 def download(src: Union[str, Path], filename: Optional[str] = None) -> None:
@@ -13,7 +13,7 @@ def download(src: Union[str, Path], filename: Optional[str] = None) -> None:
     :param filename: name of the file to download (default: name of the file on the server)
     """
     if helpers.is_file(src):
-        src = globals.app.add_static_file(local_file=src, single_use=True)
+        src = core.app.add_static_file(local_file=src, single_use=True)
     else:
         src = str(src)
     context.get_client().download(src, filename)

+ 3 - 3
nicegui/functions/refreshable.py

@@ -5,7 +5,7 @@ from typing import Any, Awaitable, Callable, ClassVar, Dict, List, Optional, Tup
 
 from typing_extensions import Self
 
-from .. import background_tasks, globals  # pylint: disable=redefined-builtin
+from .. import background_tasks, core
 from ..client import Client
 from ..dataclasses import KWONLY_SLOTS
 from ..element import Element
@@ -102,10 +102,10 @@ class refreshable:
                 raise
             if is_coroutine_function(self.func):
                 assert result is not None
-                if globals.loop and globals.loop.is_running():
+                if core.loop and core.loop.is_running():
                     background_tasks.create(result)
                 else:
-                    globals.app.on_startup(result)
+                    core.app.on_startup(result)
 
     def prune(self) -> None:
         """Remove all targets that are no longer on a page with a client connection.

+ 0 - 40
nicegui/globals.py

@@ -1,40 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-from pathlib import Path
-from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Union
-
-from socketio import AsyncServer
-
-if TYPE_CHECKING:
-    from .app import App
-    from .language import Language
-
-app: App
-sio: AsyncServer
-loop: Optional[asyncio.AbstractEventLoop] = None
-ui_run_has_been_called: bool = False
-
-reload: bool
-title: str
-viewport: str
-favicon: Optional[Union[str, Path]]
-dark: Optional[bool]
-language: Language
-binding_refresh_interval: float
-reconnect_timeout: float
-tailwind: bool
-prod_js: bool
-endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none'
-socket_io_js_query_params: Dict = {}
-socket_io_js_extra_headers: Dict = {}
-socket_io_js_transports: List[Literal['websocket', 'polling']] = ['websocket', 'polling']  # NOTE: we favor websocket
-quasar_config: Dict = {
-    'brand': {
-        'primary': '#5898d4',
-    },
-    'loadingBar': {
-        'color': 'primary',
-        'skipHijack': False,
-    },
-}

+ 2 - 2
nicegui/helpers.py

@@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Generator, Optional,
 from fastapi import Request
 from fastapi.responses import StreamingResponse
 
-from . import background_tasks, globals  # pylint: disable=redefined-builtin
+from . import background_tasks, core
 
 if TYPE_CHECKING:
     from .client import Client
@@ -75,7 +75,7 @@ def safe_invoke(func: Union[Callable[..., Any], Awaitable], client: Optional[Cli
                         await result
                 background_tasks.create(result_with_client())
     except Exception as e:
-        globals.app.handle_exception(e)
+        core.app.handle_exception(e)
 
 
 def is_port_open(host: str, port: int) -> bool:

+ 4 - 4
nicegui/native/native_mode.py

@@ -11,7 +11,7 @@ import warnings
 from threading import Event, Thread
 from typing import Any, Callable, Dict, List, Tuple
 
-from .. import globals, helpers, optional_features  # pylint: disable=redefined-builtin
+from .. import core, helpers, optional_features
 from ..logging import log
 from ..server import Server
 from . import native
@@ -40,13 +40,13 @@ def _open_window(
         'height': height,
         'fullscreen': fullscreen,
         'frameless': frameless,
-        **globals.app.native.window_args,
+        **core.app.native.window_args,
     }
     window = webview.create_window(**window_kwargs)
     closed = Event()
     window.events.closed += closed.set
     _start_window_method_executor(window, method_queue, response_queue, closed)
-    webview.start(storage_path=tempfile.mkdtemp(), **globals.app.native.start_args)
+    webview.start(storage_path=tempfile.mkdtemp(), **core.app.native.start_args)
 
 
 def _start_window_method_executor(window: webview.Window,
@@ -100,7 +100,7 @@ def activate(host: str, port: int, title: str, width: int, height: int, fullscre
         while process.is_alive():
             time.sleep(0.1)
         Server.instance.should_exit = True
-        while not globals.app.is_stopped:
+        while not core.app.is_stopped:
             time.sleep(0.1)
         _thread.interrupt_main()
 

+ 12 - 14
nicegui/nicegui.py

@@ -10,13 +10,11 @@ from fastapi.responses import FileResponse, Response
 from fastapi.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 
-from . import (air, background_tasks, binding, favicon, globals, json, outbox, run,  # pylint: disable=redefined-builtin
-               welcome)
+from . import air, background_tasks, binding, core, favicon, helpers, json, outbox, run, welcome
 from .app import App
 from .client import Client
 from .dependencies import js_components, libraries
 from .error import error_content
-from .helpers import is_file
 from .json import NiceGUIJSONResponse
 from .logging import log
 from .middlewares import RedirectWithPrefixMiddleware
@@ -24,10 +22,10 @@ from .page import page
 from .slot import Slot
 from .version import __version__
 
-globals.app = app = App(default_response_class=NiceGUIJSONResponse)
+core.app = app = App(default_response_class=NiceGUIJSONResponse)
 # NOTE we use custom json module which wraps orjson
 socket_manager = SocketManager(app=app, mount_location='/_nicegui_ws/', json=json)
-globals.sio = sio = socket_manager._sio  # pylint: disable=protected-access
+core.sio = sio = socket_manager._sio  # pylint: disable=protected-access
 
 mimetypes.add_type('text/javascript', '.js')
 mimetypes.add_type('text/css', '.css')
@@ -74,9 +72,9 @@ def _get_component(key: str) -> FileResponse:
 def handle_startup(with_welcome_message: bool = True) -> None:
     """Handle the startup event."""
     # NOTE ping interval and timeout need to be lower than the reconnect timeout, but can't be too low
-    globals.sio.eio.ping_interval = max(globals.reconnect_timeout * 0.8, 4)
-    globals.sio.eio.ping_timeout = max(globals.reconnect_timeout * 0.4, 2)
-    if not globals.ui_run_has_been_called:
+    sio.eio.ping_interval = max(app.config.reconnect_timeout * 0.8, 4)
+    sio.eio.ping_timeout = max(app.config.reconnect_timeout * 0.4, 2)
+    if not hasattr(app, 'config'):
         raise RuntimeError('\n\n'
                            'You must call ui.run() to start the server.\n'
                            'If ui.run() is behind a main guard\n'
@@ -84,15 +82,15 @@ def handle_startup(with_welcome_message: bool = True) -> None:
                            'remove the guard or replace it with\n'
                            '   if __name__ in {"__main__", "__mp_main__"}:\n'
                            'to allow for multiprocessing.')
-    if globals.favicon:
-        if is_file(globals.favicon):
-            app.add_route('/favicon.ico', lambda _: FileResponse(globals.favicon))  # type: ignore
+    if app.config.favicon:
+        if helpers.is_file(app.config.favicon):
+            app.add_route('/favicon.ico', lambda _: FileResponse(app.config.favicon))  # type: ignore
         else:
             app.add_route('/favicon.ico', lambda _: favicon.get_favicon_response())
     else:
         app.add_route('/favicon.ico', lambda _: FileResponse(Path(__file__).parent / 'static' / 'favicon.ico'))
-    globals.loop = asyncio.get_running_loop()
-    globals.app.start()
+    core.loop = asyncio.get_running_loop()
+    app.start()
     background_tasks.create(binding.refresh_loop(), name='refresh bindings')
     background_tasks.create(outbox.loop(air.instance), name='send outbox')
     background_tasks.create(Client.prune_instances(), name='prune clients')
@@ -107,7 +105,7 @@ async def handle_shutdown() -> None:
     """Handle the shutdown event."""
     if app.native.main_window:
         app.native.main_window.signal_server_shutdown()
-    globals.app.stop()
+    app.stop()
     run.tear_down()
     air.disconnect()
 

+ 4 - 4
nicegui/outbox.py

@@ -4,7 +4,7 @@ import asyncio
 from collections import defaultdict, deque
 from typing import TYPE_CHECKING, Any, DefaultDict, Deque, Dict, Optional, Tuple
 
-from . import globals  # pylint: disable=redefined-builtin
+from . import core
 
 if TYPE_CHECKING:
     from .air import Air
@@ -37,7 +37,7 @@ def enqueue_message(message_type: MessageType, data: Any, target_id: ClientId) -
 async def loop(air: Optional[Air]) -> None:
     """Emit queued updates and messages in an endless loop."""
     async def emit(message_type: MessageType, data: Any, target_id: ClientId) -> None:
-        await globals.sio.emit(message_type, data, room=target_id)
+        await core.sio.emit(message_type, data, room=target_id)
         if air is not None and air.is_air_target(target_id):
             await air.emit(message_type, data, room=target_id)
 
@@ -64,7 +64,7 @@ async def loop(air: Optional[Air]) -> None:
                 try:
                     await coro
                 except Exception as e:
-                    globals.app.handle_exception(e)
+                    core.app.handle_exception(e)
         except Exception as e:
-            globals.app.handle_exception(e)
+            core.app.handle_exception(e)
             await asyncio.sleep(0.1)

+ 8 - 8
nicegui/page.py

@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Callable, Optional, Union
 
 from fastapi import Request, Response
 
-from . import background_tasks, binding, globals, helpers  # pylint: disable=redefined-builtin
+from . import background_tasks, binding, core, helpers
 from .client import Client
 from .favicon import create_favicon_route
 from .language import Language
@@ -57,7 +57,7 @@ class page:
         self.language = language
         self.response_timeout = response_timeout
         self.kwargs = kwargs
-        self.api_router = api_router or globals.app.router
+        self.api_router = api_router or core.app.router
         self.reconnect_timeout = reconnect_timeout
 
         create_favicon_route(self.path, favicon)
@@ -69,22 +69,22 @@ class page:
 
     def resolve_title(self) -> str:
         """Return the title of the page."""
-        return self.title if self.title is not None else globals.title
+        return self.title if self.title is not None else core.app.config.title
 
     def resolve_viewport(self) -> str:
         """Return the viewport of the page."""
-        return self.viewport if self.viewport is not None else globals.viewport
+        return self.viewport if self.viewport is not None else core.app.config.viewport
 
     def resolve_dark(self) -> Optional[bool]:
         """Return whether the page should use dark mode."""
-        return self.dark if self.dark is not ... else globals.dark
+        return self.dark if self.dark is not ... else core.app.config.dark
 
     def resolve_language(self) -> Optional[str]:
         """Return the language of the page."""
-        return self.language if self.language is not ... else globals.language
+        return self.language if self.language is not ... else core.app.config.language
 
     def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
-        globals.app.remove_route(self.path)  # NOTE make sure only the latest route definition is used
+        core.app.remove_route(self.path)  # NOTE make sure only the latest route definition is used
         parameters_of_decorated_func = list(inspect.signature(func).parameters.keys())
 
         async def decorated(*dec_args, **dec_kwargs) -> Response:
@@ -119,7 +119,7 @@ class page:
         decorated.__signature__ = inspect.Signature(parameters)  # type: ignore
 
         if 'include_in_schema' not in self.kwargs:
-            self.kwargs['include_in_schema'] = globals.endpoint_documentation in {'page', 'all'}
+            self.kwargs['include_in_schema'] = core.app.extra_config.endpoint_documentation in {'page', 'all'}
 
         self.api_router.get(self._path, **self.kwargs)(decorated)
         Client.page_routes[func] = self.path

+ 2 - 2
nicegui/run.py

@@ -4,14 +4,14 @@ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
 from functools import partial
 from typing import Any, Callable
 
-from . import globals, helpers  # pylint: disable=redefined-builtin
+from . import core, helpers
 
 process_pool = ProcessPoolExecutor()
 thread_pool = ThreadPoolExecutor()
 
 
 async def _run(executor: Any, callback: Callable, *args: Any, **kwargs: Any) -> Any:
-    if globals.app.is_stopping:
+    if core.app.is_stopping:
         return
     try:
         loop = asyncio.get_running_loop()

+ 2 - 3
nicegui/server.py

@@ -6,8 +6,7 @@ from typing import List, Optional
 
 import uvicorn
 
-from . import globals  # pylint: disable=redefined-builtin
-from . import storage  # pylint: disable=redefined-builtin
+from . import core, storage
 from .native import native
 
 
@@ -29,7 +28,7 @@ class Server(uvicorn.Server):
         self.instance = self
         assert isinstance(self.config, CustomServerConfig)
         if self.config.method_queue is not None and self.config.response_queue is not None:
-            globals.app.native.main_window = native.WindowProxy()
+            core.app.native.main_window = native.WindowProxy()
             native.method_queue = self.config.method_queue
             native.response_queue = self.config.response_queue
 

+ 7 - 7
nicegui/storage.py

@@ -12,7 +12,7 @@ from starlette.middleware.sessions import SessionMiddleware
 from starlette.requests import Request
 from starlette.responses import Response
 
-from . import background_tasks, context, globals, json, observables  # pylint: disable=redefined-builtin
+from . import background_tasks, context, core, json, observables
 
 request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None)
 
@@ -56,10 +56,10 @@ class PersistentDict(observables.ObservableDict):
         async def backup() -> None:
             async with aiofiles.open(self.filepath, 'w') as f:
                 await f.write(json.dumps(self))
-        if globals.loop:
+        if core.loop:
             background_tasks.create_lazy(backup(), name=self.filepath.stem)
         else:
-            globals.app.on_startup(backup())
+            core.app.on_startup(backup())
 
 
 class RequestTrackingMiddleware(BaseHTTPMiddleware):
@@ -76,12 +76,12 @@ class RequestTrackingMiddleware(BaseHTTPMiddleware):
 
 def set_storage_secret(storage_secret: Optional[str] = None) -> None:
     """Set storage_secret and add request tracking middleware."""
-    if any(m.cls == SessionMiddleware for m in globals.app.user_middleware):
+    if any(m.cls == SessionMiddleware for m in core.app.user_middleware):
         # NOTE not using "add_middleware" because it would be the wrong order
-        globals.app.user_middleware.append(Middleware(RequestTrackingMiddleware))
+        core.app.user_middleware.append(Middleware(RequestTrackingMiddleware))
     elif storage_secret is not None:
-        globals.app.add_middleware(RequestTrackingMiddleware)
-        globals.app.add_middleware(SessionMiddleware, secret_key=storage_secret)
+        core.app.add_middleware(RequestTrackingMiddleware)
+        core.app.add_middleware(SessionMiddleware, secret_key=storage_secret)
 
 
 class Storage:

+ 22 - 19
nicegui/ui_run.py

@@ -9,8 +9,9 @@ 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 air, core, helpers
 from . import native as native_module
+from .app_config import AppConfig
 from .client import Client
 from .language import Language
 from .logging import log
@@ -46,7 +47,7 @@ def run(*,
         storage_secret: Optional[str] = None,
         **kwargs: Any,
         ) -> None:
-    '''ui.run
+    """ui.run
 
     You can call `ui.run()` with optional arguments:
 
@@ -75,21 +76,22 @@ def run(*,
     :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:
+    """
+    core.app.config = AppConfig(
+        reload=reload,
+        title=title,
+        viewport=viewport,
+        favicon=favicon,
+        dark=dark,
+        language=language,
+        binding_refresh_interval=binding_refresh_interval,
+        reconnect_timeout=reconnect_timeout,
+        tailwind=tailwind,
+        prod_js=prod_js,
+    )
+    core.app.extra_config.endpoint_documentation = endpoint_documentation
+
+    for route in core.app.routes:
         if not isinstance(route, Route):
             continue
         if route.path.startswith('/_nicegui') and hasattr(route, 'methods'):
@@ -105,7 +107,7 @@ def run(*,
 
     if reload and not hasattr(__main__, '__file__'):
         log.warning('auto-reloading is only supported when running from a file')
-        globals.reload = reload = False
+        core.app.config.reload = reload = False
 
     if fullscreen:
         native = True
@@ -121,6 +123,7 @@ def run(*,
         native_module.activate(host, port, title, width, height, fullscreen, frameless)
     else:
         host = host or '0.0.0.0'
+    assert host is not None
 
     # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
     os.environ['NICEGUI_HOST'] = host
@@ -135,7 +138,7 @@ def run(*,
     # 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,
+        APP_IMPORT_STRING if reload else core.app,
         host=host,
         port=port,
         reload=reload,

+ 15 - 12
nicegui/ui_run_with.py

@@ -3,7 +3,8 @@ from typing import Optional, Union
 
 from fastapi import FastAPI
 
-from . import globals, storage  # pylint: disable=redefined-builtin
+from . import core, storage
+from .app_config import AppConfig
 from .language import Language
 from .nicegui import handle_shutdown, handle_startup
 
@@ -37,19 +38,21 @@ def run_with(
     :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
     :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
     """
-    globals.ui_run_has_been_called = True
-    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
+    core.app.config = AppConfig(
+        reload=False,
+        title=title,
+        viewport=viewport,
+        favicon=favicon,
+        dark=dark,
+        language=language,
+        binding_refresh_interval=binding_refresh_interval,
+        reconnect_timeout=reconnect_timeout,
+        tailwind=tailwind,
+        prod_js=prod_js,
+    )
 
     storage.set_storage_secret(storage_secret)
     app.on_event('startup')(lambda: handle_startup(with_welcome_message=False))
     app.on_event('shutdown')(handle_shutdown)
 
-    app.mount(mount_path, globals.app)
+    app.mount(mount_path, core.app)

+ 2 - 3
nicegui/welcome.py

@@ -2,8 +2,7 @@ import os
 import socket
 from typing import List
 
-from . import globals  # pylint: disable=redefined-builtin
-from . import optional_features, run
+from . import core, optional_features, run
 
 try:
     import netifaces
@@ -37,7 +36,7 @@ async def print_message() -> None:
     ips = set((await run.io_bound(_get_all_ips)) if host == '0.0.0.0' else [])
     ips.discard('127.0.0.1')
     urls = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
-    globals.app.urls.update(urls)
+    core.app.urls.update(urls)
     if len(urls) >= 2:
         urls[-1] = 'and ' + urls[-1]
     extra = ''

+ 10 - 11
tests/conftest.py

@@ -9,8 +9,7 @@ import pytest
 from selenium import webdriver
 from selenium.webdriver.chrome.service import Service
 
-from nicegui import Client, binding, globals  # pylint: disable=redefined-builtin
-from nicegui.elements import plotly, pyplot
+from nicegui import Client, app, binding, core
 from nicegui.page import page
 
 from .screen import Screen
@@ -46,20 +45,20 @@ def capabilities(capabilities: Dict) -> Dict:
 @pytest.fixture(autouse=True)
 def reset_globals() -> Generator[None, None, None]:
     for path in {'/'}.union(Client.page_routes.values()):
-        globals.app.remove_route(path)
-    globals.app.openapi_schema = None
-    globals.app.middleware_stack = None
-    globals.app.user_middleware.clear()
+        app.remove_route(path)
+    app.openapi_schema = None
+    app.middleware_stack = None
+    app.user_middleware.clear()
     # NOTE favicon routes must be removed separately because they are not "pages"
-    for route in globals.app.routes:
+    for route in app.routes:
         if route.path.endswith('/favicon.ico'):
-            globals.app.routes.remove(route)
-    importlib.reload(globals)
+            app.routes.remove(route)
+    importlib.reload(core)
     Client.instances.clear()
     Client.page_routes.clear()
     Client.index_client = Client(page('/'), shared=True).__enter__()
-    globals.app.reset()
-    globals.app.get('/')(Client.index_client.build_response)
+    app.reset()
+    app.get('/')(Client.index_client.build_response)
     binding.reset()
 
 

+ 3 - 3
website/more_documentation/query_documentation.py

@@ -1,4 +1,4 @@
-from nicegui import globals, ui
+from nicegui import context, ui
 
 from ..documentation_tools import text_demo
 
@@ -22,7 +22,7 @@ def more() -> None:
     def background_image():
         # ui.query('body').classes('bg-gradient-to-t from-blue-400 to-blue-100')
         # END OF DEMO
-        globals.get_slot_stack()[-1].parent.classes('bg-gradient-to-t from-blue-400 to-blue-100')
+        context.get_slot_stack()[-1].parent.classes('bg-gradient-to-t from-blue-400 to-blue-100')
 
     @text_demo('Modify default page padding', '''
         By default, NiceGUI provides a built-in padding around the content of the page.
@@ -30,7 +30,7 @@ def more() -> None:
     ''')
     def remove_padding():
         # ui.query('.nicegui-content').classes('p-0')
-        globals.get_slot_stack()[-1].parent.classes(remove='p-4')  # HIDE
+        context.get_slot_stack()[-1].parent.classes(remove='p-4')  # HIDE
         # with ui.column().classes('h-screen w-full bg-gray-400 justify-between'):
         with ui.column().classes('h-full w-full bg-gray-400 justify-between'):  # HIDE
             ui.label('top left')