From 67c6c519ab7df8fe5daa4d794a974e35cdd725b0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 27 Nov 2023 12:24:39 +0000 Subject: [PATCH] Lazy command privoder --- CHANGELOG.md | 2 +- src/textual/app.py | 17 +++++++++++++++-- src/textual/command.py | 22 ++++++++++++++++++++-- src/textual/screen.py | 2 +- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8287fad47e..3832063cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Optimized startup time https://github.com/Textualize/textual/pull/3753 - +- App.COMMANDS or Screen.COMMANDS can now accept a callable which returns a command palette privoder ## [0.42.0] - 2023-11-22 diff --git a/src/textual/app.py b/src/textual/app.py index 9006feffd2..dbadf00f8f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -67,7 +67,6 @@ from ._context import message_hook as message_hook_context_var from ._event_broker import NoHandler, extract_handler_actions from ._path import CSSPathType, _css_path_type_as_list, _make_path_object_relative -from ._system_commands import SystemCommands from ._wait import wait_for_idle from ._worker_manager import WorkerManager from .actions import ActionParseResult, SkipAction @@ -107,6 +106,7 @@ from textual_dev.client import DevtoolsClient from typing_extensions import Coroutine, Literal, TypeAlias + from ._system_commands import SystemCommands from ._types import MessageTarget # Unused & ignored imports are needed for the docs to link to these objects: @@ -158,6 +158,17 @@ """Signature for valid callbacks that can be used to control apps.""" +def get_system_commands() -> type[SystemCommands]: + """Callable to lazy load the system commands. + + Returns: + System commands class. + """ + from ._system_commands import SystemCommands + + return SystemCommands + + class AppError(Exception): """Base class for general App related exceptions.""" @@ -330,7 +341,9 @@ class MyApp(App[None]): ENABLE_COMMAND_PALETTE: ClassVar[bool] = True """Should the [command palette][textual.command.CommandPalette] be enabled for the application?""" - COMMANDS: ClassVar[set[type[Provider]]] = {SystemCommands} + COMMANDS: ClassVar[set[type[Provider] | Callable[[], type[Provider]]]] = { + get_system_commands + } """Command providers used by the [command palette](/guide/command_palette). Should be a set of [command.Provider][textual.command.Provider] classes. diff --git a/src/textual/command.py b/src/textual/command.py index ef71f9b33c..a0f4ecfb72 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -10,8 +10,9 @@ from asyncio import CancelledError, Queue, Task, TimeoutError, wait, wait_for from dataclasses import dataclass from functools import total_ordering +from inspect import isclass from time import monotonic -from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, ClassVar +from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, ClassVar, Iterable import rich.repr from rich.align import Align @@ -486,10 +487,27 @@ def _provider_classes(self) -> set[type[Provider]]: application][textual.app.App.COMMANDS] and those [defined in the current screen][textual.screen.Screen.COMMANDS]. """ + + def get_providers(root: App | Screen) -> Iterable[type[Provider]]: + """Get providers from app or screen. + + Args: + root: The app or screen. + + Returns: + An iterable of providers. + """ + for provider in root.COMMANDS: + if isclass(provider) and issubclass(provider, Provider): + yield provider + else: + # Lazy loaded providers + yield provider() # type: ignore + return ( set() if self._calling_screen is None - else self.app.COMMANDS | self._calling_screen.COMMANDS + else {*get_providers(self.app), *get_providers(self._calling_screen)} ) def compose(self) -> ComposeResult: diff --git a/src/textual/screen.py b/src/textual/screen.py index 7ed6c72231..4387e62b44 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -164,7 +164,7 @@ class Screen(Generic[ScreenResultType], Widget): title: Reactive[str | None] = Reactive(None, compute=False) """Screen title to override [the app title][textual.app.App.title].""" - COMMANDS: ClassVar[set[type[Provider]]] = set() + COMMANDS: ClassVar[set[type[Provider] | Callable[[], type[Provider]]]] = set() """Command providers used by the [command palette](/guide/command_palette), associated with the screen. Should be a set of [`command.Provider`][textual.command.Provider] classes.