Browse Source

remove logging and livecycle hooks from globals.py

Falko Schindler 1 year ago
parent
commit
db59f17ef2

+ 2 - 0
nicegui/__init__.py

@@ -4,6 +4,7 @@ from . import run_executor as run
 from .api_router import APIRouter
 from .awaitable_response import AwaitableResponse
 from .client import Client
+from .logging import log
 from .nicegui import app
 from .tailwind import Tailwind
 from .version import __version__
@@ -15,6 +16,7 @@ __all__ = [
     'Client',
     'elements',
     'globals',
+    'log',
     'run',
     'Tailwind',
     'ui',

+ 2 - 1
nicegui/air.py

@@ -8,6 +8,7 @@ import socketio
 from socketio import AsyncClient
 
 from . import background_tasks, globals  # pylint: disable=redefined-builtin
+from .logging import log
 from .nicegui import handle_disconnect, handle_event, handle_handshake, handle_javascript_response
 
 RELAY_HOST = 'https://on-air.nicegui.io/'
@@ -127,7 +128,7 @@ class Air:
             except ValueError:  # NOTE this sometimes happens when the internal socketio client is not yet ready
                 await self.relay.disconnect()
             except Exception:
-                globals.log.exception('Could not connect to NiceGUI On Air server.')
+                log.exception('Could not connect to NiceGUI On Air server.')
 
             await asyncio.sleep(backoff_time)
             backoff_time = min(backoff_time * 2, 32)

+ 23 - 8
nicegui/app.py

@@ -1,11 +1,13 @@
+import inspect
 from pathlib import Path
-from typing import Awaitable, Callable, Optional, Union
+from typing import Any, Awaitable, Callable, List, Optional, Union
 
 from fastapi import FastAPI, HTTPException, Request
 from fastapi.responses import FileResponse, StreamingResponse
 from fastapi.staticfiles import StaticFiles
 
-from . import globals, helpers  # pylint: disable=redefined-builtin
+from . import background_tasks, globals, helpers  # pylint: disable=redefined-builtin
+from .logging import log
 from .native import Native
 from .observables import ObservableSet
 from .storage import Storage
@@ -19,19 +21,25 @@ class App(FastAPI):
         self.storage = Storage()
         self.urls = ObservableSet()
 
+        self._startup_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+        self._shutdown_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+        self._connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+        self._disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+        self._exception_handlers: List[Callable[..., Any]] = [log.exception]
+
     def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
         """Called every time a new client connects to NiceGUI.
 
         The callback has an optional parameter of `nicegui.Client`.
         """
-        globals.connect_handlers.append(handler)
+        self._connect_handlers.append(handler)
 
     def on_disconnect(self, handler: Union[Callable, Awaitable]) -> None:
         """Called every time a new client disconnects from NiceGUI.
 
         The callback has an optional parameter of `nicegui.Client`.
         """
-        globals.disconnect_handlers.append(handler)
+        self._disconnect_handlers.append(handler)
 
     def on_startup(self, handler: Union[Callable, Awaitable]) -> None:
         """Called when NiceGUI is started or restarted.
@@ -40,21 +48,28 @@ class App(FastAPI):
         """
         if globals.state == globals.State.STARTED:
             raise RuntimeError('Unable to register another startup handler. NiceGUI has already been started.')
-        globals.startup_handlers.append(handler)
+        self._startup_handlers.append(handler)
 
     def on_shutdown(self, handler: Union[Callable, Awaitable]) -> None:
         """Called when NiceGUI is shut down or restarted.
 
         When NiceGUI is shut down or restarted, all tasks still in execution will be automatically canceled.
         """
-        globals.shutdown_handlers.append(handler)
+        self._shutdown_handlers.append(handler)
 
     def on_exception(self, handler: Callable) -> None:
         """Called when an exception occurs.
 
         The callback has an optional parameter of `Exception`.
         """
-        globals.exception_handlers.append(handler)
+        self._exception_handlers.append(handler)
+
+    def handle_exception(self, exception: Exception) -> None:
+        """Handle an exception by invoking all registered exception handlers."""
+        for handler in self._exception_handlers:
+            result = handler() if not inspect.signature(handler).parameters else handler(exception)
+            if helpers.is_coroutine_function(handler):
+                background_tasks.create(result)
 
     def shutdown(self) -> None:
         """Shut down NiceGUI.
@@ -85,7 +100,7 @@ class App(FastAPI):
         """
         if url_path == '/':
             raise ValueError('''Path cannot be "/", because it would hide NiceGUI's internal "/_nicegui" route.''')
-        globals.app.mount(url_path, StaticFiles(directory=str(local_directory)))
+        self.mount(url_path, StaticFiles(directory=str(local_directory)))
 
     def add_static_file(self, *,
                         local_file: Union[str, Path],

+ 1 - 1
nicegui/background_tasks.py

@@ -57,4 +57,4 @@ def _handle_task_result(task: asyncio.Task) -> None:
     except asyncio.CancelledError:
         pass
     except Exception as e:
-        globals.handle_exception(e)
+        globals.app.handle_exception(e)

+ 2 - 1
nicegui/binding.py

@@ -5,6 +5,7 @@ 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 .logging import log
 
 MAX_PROPAGATION_TIME = 0.01
 
@@ -51,7 +52,7 @@ def _refresh_step() -> None:
                 _propagate(target_obj, target_name, visited)
         del link, source_obj, target_obj  # pylint: disable=modified-iterating-list
     if time.time() - t > MAX_PROPAGATION_TIME:
-        globals.log.warning(f'binding propagation for {len(active_links)} active links took {time.time() - t:.3f} s')
+        log.warning(f'binding propagation for {len(active_links)} active links took {time.time() - t:.3f} s')
 
 
 def _propagate(source_obj: Any, source_name: str, visited: Optional[Set[Tuple[int, str]]] = None) -> None:

+ 4 - 3
nicegui/client.py

@@ -17,6 +17,7 @@ from .awaitable_response import AwaitableResponse
 from .dependencies import generate_resources
 from .element import Element
 from .favicon import get_favicon_url
+from .logging import log
 from .version import __version__
 
 if TYPE_CHECKING:
@@ -135,9 +136,9 @@ class Client:
         You can do this by `await client.connected()` or register a callback with `client.on_connect(...)`.
         """
         if respond is True:
-            globals.log.warning('The "respond" argument of run_javascript() has been removed. '
-                                'Now the method always returns an AwaitableResponse that can be awaited. '
-                                'Please remove the "respond=True" argument.')
+            log.warning('The "respond" argument of run_javascript() has been removed. '
+                        'Now the method always returns an AwaitableResponse that can be awaited. '
+                        'Please remove the "respond=True" argument.')
         if respond is False:
             raise ValueError('The "respond" argument of run_javascript() has been removed. '
                              'Now the method always returns an AwaitableResponse that can be awaited. '

+ 3 - 2
nicegui/elements/menu.py

@@ -4,6 +4,7 @@ from typing_extensions import Self
 
 from .. import globals  # pylint: disable=redefined-builtin
 from ..events import ClickEventArguments, handle_event
+from ..logging import log
 from .context_menu import ContextMenu
 from .mixins.text_element import TextElement
 from .mixins.value_element import ValueElement
@@ -38,8 +39,8 @@ class Menu(ValueElement):
         if 'touch-position' in self._props:
             # https://github.com/zauberzeug/nicegui/issues/1738
             del self._props['touch-position']
-            globals.log.warning('The prop "touch-position" is not supported by `ui.menu`.\n'
-                                'Use "ui.context_menu()" instead.')
+            log.warning('The prop "touch-position" is not supported by `ui.menu`.\n'
+                        'Use "ui.context_menu()" instead.')
         return self
 
 

+ 4 - 3
nicegui/elements/timer.py

@@ -5,6 +5,7 @@ from typing import Any, Callable, Optional
 from .. import background_tasks, globals, helpers  # pylint: disable=redefined-builtin
 from ..binding import BindableProperty
 from ..element import Element
+from ..logging import log
 
 
 class Timer(Element, component='timer.js'):
@@ -79,7 +80,7 @@ class Timer(Element, component='timer.js'):
                     except asyncio.CancelledError:
                         break
                     except Exception as e:
-                        globals.handle_exception(e)
+                        globals.app.handle_exception(e)
                         await asyncio.sleep(self.interval)
         finally:
             self._cleanup()
@@ -91,7 +92,7 @@ class Timer(Element, component='timer.js'):
             if helpers.is_coroutine_function(self.callback):
                 await result
         except Exception as e:
-            globals.handle_exception(e)
+            globals.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.
@@ -107,7 +108,7 @@ class Timer(Element, component='timer.js'):
             await self.client.connected(timeout=timeout)
             return True
         except TimeoutError:
-            globals.log.error(f'Timer cancelled because client is not connected after {timeout} seconds')
+            log.error(f'Timer cancelled because client is not connected after {timeout} seconds')
             return False
 
     def _should_stop(self) -> bool:

+ 3 - 3
nicegui/elements/tree.py

@@ -2,9 +2,9 @@ from typing import Any, Callable, Dict, Iterator, List, Literal, Optional, Set
 
 from typing_extensions import Self
 
-from .. import globals  # pylint: disable=redefined-builtin
 from ..element import Element
 from ..events import GenericEventArguments, ValueChangeEventArguments, handle_event
+from ..logging import log
 
 
 class Tree(Element):
@@ -104,6 +104,6 @@ class Tree(Element):
         if 'default-expand-all' in self._props:
             # https://github.com/zauberzeug/nicegui/issues/1385
             del self._props['default-expand-all']
-            globals.log.warning('The prop "default_expand_all" is not supported by `ui.tree`.\n'
-                                'Use ".expand()" instead.')
+            log.warning('The prop "default_expand_all" is not supported by `ui.tree`.\n'
+                        'Use ".expand()" instead.')
         return self

+ 2 - 2
nicegui/events.py

@@ -440,10 +440,10 @@ def handle_event(handler: Optional[Callable[..., Any]], arguments: EventArgument
                     try:
                         await result
                     except Exception as e:
-                        globals.handle_exception(e)
+                        globals.app.handle_exception(e)
             if globals.loop and globals.loop.is_running():
                 background_tasks.create(wait_for_result(), name=str(handler))
             else:
                 globals.app.on_startup(wait_for_result())
     except Exception as e:
-        globals.handle_exception(e)
+        globals.app.handle_exception(e)

+ 4 - 3
nicegui/functions/javascript.py

@@ -2,6 +2,7 @@ from typing import Optional
 
 from .. import globals  # pylint: disable=redefined-builtin
 from ..awaitable_response import AwaitableResponse
+from ..logging import log
 
 
 def run_javascript(code: str, *,
@@ -21,9 +22,9 @@ def run_javascript(code: str, *,
     :return: response from the browser, or `None` if `respond` is `False`
     """
     if respond is True:
-        globals.log.warning('The "respond" argument of run_javascript() has been removed. '
-                            'Now the function always returns an AwaitableResponse that can be awaited. '
-                            'Please remove the "respond=True" argument.')
+        log.warning('The "respond" argument of run_javascript() has been removed. '
+                    'Now the function always returns an AwaitableResponse that can be awaited. '
+                    'Please remove the "respond=True" argument.')
     if respond is False:
         raise ValueError('The "respond" argument of run_javascript() has been removed. '
                          'Now the function always returns an AwaitableResponse that can be awaited. '

+ 2 - 1
nicegui/functions/notify.py

@@ -1,6 +1,7 @@
 from typing import Any, Literal, Optional, Union
 
 from .. import globals, outbox  # pylint: disable=redefined-builtin
+from ..logging import log
 
 ARG_MAP = {
     'close_button': 'closeBtn',
@@ -52,4 +53,4 @@ def notify(message: Any, *,
     if globals.get_client().has_socket_connection:
         outbox.enqueue_message('notify', options, globals.get_client().id)
     else:
-        globals.log.warning(f'Ignoring notification "{message}" because the client is not connected.')
+        log.warning(f'Ignoring notification "{message}" because the client is not connected.')

+ 2 - 1
nicegui/functions/open.py

@@ -1,6 +1,7 @@
 from typing import Any, Callable, Union
 
 from .. import globals  # pylint: disable=redefined-builtin
+from ..logging import log
 
 
 def open(target: Union[Callable[..., Any], str], new_tab: bool = False) -> None:  # pylint: disable=redefined-builtin
@@ -24,4 +25,4 @@ def open(target: Union[Callable[..., Any], str], new_tab: bool = False) -> None:
     if client.has_socket_connection:
         client.open(path, new_tab)
     else:
-        globals.log.error('Cannot open page because client is not connected, try RedirectResponse from FastAPI instead')
+        log.error('Cannot open page because client is not connected, try RedirectResponse from FastAPI instead')

+ 1 - 21
nicegui/globals.py

@@ -1,20 +1,15 @@
 from __future__ import annotations
 
 import asyncio
-import inspect
-import logging
 import os
 from contextlib import contextmanager
 from enum import Enum
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Iterator, List, Literal, Optional, Set, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Literal, Optional, Set, Union
 
 from socketio import AsyncServer
 from uvicorn import Server
 
-from . import background_tasks
-from .helpers import is_coroutine_function
-
 if TYPE_CHECKING:
     from .air import Air
     from .app import App
@@ -34,7 +29,6 @@ app: App
 sio: AsyncServer
 server: Server
 loop: Optional[asyncio.AbstractEventLoop] = None
-log: logging.Logger = logging.getLogger('nicegui')
 state: State = State.STOPPED
 ui_run_has_been_called: bool = False
 optional_features: Set[str] = set()
@@ -71,12 +65,6 @@ quasar_config: Dict = {
 
 page_routes: Dict[Callable[..., Any], str] = {}
 
-startup_handlers: List[Union[Callable[..., Any], Awaitable]] = []
-shutdown_handlers: List[Union[Callable[..., Any], Awaitable]] = []
-connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
-disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
-exception_handlers: List[Callable[..., Any]] = [log.exception]
-
 
 def get_task_id() -> int:
     """Return the ID of the current asyncio task."""
@@ -118,11 +106,3 @@ def socket_id(id_: str) -> Iterator[None]:
     _socket_id = id_
     yield
     _socket_id = None
-
-
-def handle_exception(exception: Exception) -> None:
-    """Handle an exception by invoking all registered exception handlers."""
-    for handler in exception_handlers:
-        result = handler() if not inspect.signature(handler).parameters else handler(exception)
-        if is_coroutine_function(handler):
-            background_tasks.create(result)

+ 1 - 1
nicegui/helpers.py

@@ -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.handle_exception(e)
+        globals.app.handle_exception(e)
 
 
 def is_port_open(host: str, port: int) -> bool:

+ 3 - 0
nicegui/logging.py

@@ -0,0 +1,3 @@
+import logging
+
+log: logging.Logger = logging.getLogger('nicegui')

+ 1 - 1
nicegui/native.py

@@ -5,7 +5,7 @@ from multiprocessing import Queue
 from typing import Any, Callable, Dict, Optional, Tuple
 
 from .dataclasses import KWONLY_SLOTS
-from .globals import log
+from .logging import log
 from .run_executor import io_bound
 
 method_queue: Queue = Queue()

+ 7 - 6
nicegui/native_mode.py

@@ -12,6 +12,7 @@ from threading import Event, Thread
 from typing import Any, Callable, Dict, List, Tuple
 
 from . import globals, helpers, native  # pylint: disable=redefined-builtin
+from .logging import log
 
 try:
     with warnings.catch_warnings():
@@ -56,7 +57,7 @@ def _start_window_method_executor(window: webview.Window,
             if response is not None or 'dialog' in method.__name__:
                 response_queue.put(response)
         except Exception:
-            globals.log.exception(f'error in window.{method.__name__}')
+            log.exception(f'error in window.{method.__name__}')
 
     def window_method_executor() -> None:
         pending_executions: List[Thread] = []
@@ -65,7 +66,7 @@ def _start_window_method_executor(window: webview.Window,
                 method_name, args, kwargs = method_queue.get(block=False)
                 if method_name == 'signal_server_shutdown':
                     if pending_executions:
-                        globals.log.warning('shutdown is possibly blocked by opened dialogs like a file picker')
+                        log.warning('shutdown is possibly blocked by opened dialogs like a file picker')
                         while pending_executions:
                             pending_executions.pop().join()
                 elif method_name == 'get_always_on_top':
@@ -82,11 +83,11 @@ def _start_window_method_executor(window: webview.Window,
                         pending_executions.append(Thread(target=execute, args=(method, args, kwargs)))
                         pending_executions[-1].start()
                     else:
-                        globals.log.error(f'window.{method_name} is not callable')
+                        log.error(f'window.{method_name} is not callable')
             except queue.Empty:
                 time.sleep(0.01)
             except Exception:
-                globals.log.exception(f'error in window.{method_name}')
+                log.exception(f'error in window.{method_name}')
 
     Thread(target=window_method_executor).start()
 
@@ -102,8 +103,8 @@ def activate(host: str, port: int, title: str, width: int, height: int, fullscre
         _thread.interrupt_main()
 
     if 'native' not in globals.optional_features:
-        globals.log.error('Native mode is not supported in this configuration.\n'
-                          'Please run "pip install pywebview" to use it.')
+        log.error('Native mode is not supported in this configuration.\n'
+                  'Please run "pip install pywebview" to use it.')
         sys.exit(1)
 
     mp.freeze_support()

+ 10 - 9
nicegui/nicegui.py

@@ -19,6 +19,7 @@ from .dependencies import js_components, libraries
 from .error import error_content
 from .helpers import is_file, safe_invoke
 from .json import NiceGUIJSONResponse
+from .logging import log
 from .middlewares import RedirectWithPrefixMiddleware
 from .page import page
 from .version import __version__
@@ -85,15 +86,15 @@ def handle_startup(with_welcome_message: bool = True) -> None:
                            'to allow for multiprocessing.')
     if globals.favicon:
         if is_file(globals.favicon):
-            globals.app.add_route('/favicon.ico', lambda _: FileResponse(globals.favicon))  # type: ignore
+            app.add_route('/favicon.ico', lambda _: FileResponse(globals.favicon))  # type: ignore
         else:
-            globals.app.add_route('/favicon.ico', lambda _: favicon.get_favicon_response())
+            app.add_route('/favicon.ico', lambda _: favicon.get_favicon_response())
     else:
-        globals.app.add_route('/favicon.ico', lambda _: FileResponse(Path(__file__).parent / 'static' / 'favicon.ico'))
+        app.add_route('/favicon.ico', lambda _: FileResponse(Path(__file__).parent / 'static' / 'favicon.ico'))
     globals.state = globals.State.STARTING
     globals.loop = asyncio.get_running_loop()
     with globals.index_client:
-        for t in globals.startup_handlers:
+        for t in app._startup_handlers:
             safe_invoke(t)
     background_tasks.create(binding.refresh_loop(), name='refresh bindings')
     background_tasks.create(outbox.loop(), name='send outbox')
@@ -113,7 +114,7 @@ async def handle_shutdown() -> None:
         app.native.main_window.signal_server_shutdown()
     globals.state = globals.State.STOPPING
     with globals.index_client:
-        for t in globals.shutdown_handlers:
+        for t in app._shutdown_handlers:
             safe_invoke(t)
     run_executor.tear_down()
     globals.state = globals.State.STOPPED
@@ -123,7 +124,7 @@ async def handle_shutdown() -> None:
 
 @app.exception_handler(404)
 async def _exception_handler_404(request: Request, exception: Exception) -> Response:
-    globals.log.warning(f'{request.url} not found')
+    log.warning(f'{request.url} not found')
     with Client(page('')) as client:
         error_content(404, exception)
     return client.build_response(request, 404)
@@ -131,7 +132,7 @@ async def _exception_handler_404(request: Request, exception: Exception) -> Resp
 
 @app.exception_handler(Exception)
 async def _exception_handler_500(request: Request, exception: Exception) -> Response:
-    globals.log.exception(exception)
+    log.exception(exception)
     with Client(page('')) as client:
         error_content(500, exception)
     return client.build_response(request, 500)
@@ -155,7 +156,7 @@ def handle_handshake(client: Client) -> None:
         client.disconnect_task = None
     for t in client.connect_handlers:
         safe_invoke(t, client)
-    for t in globals.connect_handlers:
+    for t in app._connect_handlers:
         safe_invoke(t, client)
 
 
@@ -177,7 +178,7 @@ async def handle_disconnect(client: Client) -> None:
         _delete_client(client.id)
     for t in client.disconnect_handlers:
         safe_invoke(t, client)
-    for t in globals.disconnect_handlers:
+    for t in app._disconnect_handlers:
         safe_invoke(t, client)
 
 

+ 2 - 2
nicegui/outbox.py

@@ -65,9 +65,9 @@ async def loop() -> None:
                 try:
                     await coro
                 except Exception as e:
-                    globals.handle_exception(e)
+                    globals.app.handle_exception(e)
         except Exception as e:
-            globals.handle_exception(e)
+            globals.app.handle_exception(e)
             await asyncio.sleep(0.1)
 
 

+ 3 - 2
nicegui/run.py

@@ -17,6 +17,7 @@ from . import globals, helpers  # pylint: disable=redefined-builtin
 from . import native as native_module
 from .air import Air
 from .language import Language
+from .logging import log
 
 APP_IMPORT_STRING = 'nicegui:app'
 
@@ -126,7 +127,7 @@ def run(*,
         return
 
     if reload and not hasattr(__main__, '__file__'):
-        globals.log.warning('auto-reloading is only supported when running from a file')
+        log.warning('auto-reloading is only supported when running from a file')
         globals.reload = reload = False
 
     if fullscreen:
@@ -173,7 +174,7 @@ def run(*,
     globals.server = Server(config=config)
 
     if (reload or config.workers > 1) and not isinstance(config.app, str):
-        globals.log.warning('You must pass the application as an import string to enable "reload" or "workers".')
+        log.warning('You must pass the application as an import string to enable "reload" or "workers".')
         sys.exit(1)
 
     if config.should_reload:

+ 1 - 1
prometheus.py

@@ -4,7 +4,7 @@ import uuid
 from fastapi import FastAPI, Request, Response
 from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
 
-from nicegui.globals import log
+from nicegui import log
 
 EXCLUDED_USER_AGENTS = {'bot', 'spider', 'crawler', 'monitor', 'curl',
                         'wget', 'python-requests', 'kuma', 'health check'}