From 67c6c519ab7df8fe5daa4d794a974e35cdd725b0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 27 Nov 2023 12:24:39 +0000 Subject: [PATCH 1/4] 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. From 1a3d99f90989bc833a6ee342e3e57772acd8d2cd Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 27 Nov 2023 12:26:30 +0000 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3832063cc1..b14cf83bbc 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 +- App.COMMANDS or Screen.COMMANDS can now accept a callable which returns a command palette provider https://github.com/Textualize/textual/pull/3756 ## [0.42.0] - 2023-11-22 From 2b53609bfafceabe9885a45634b3688503c57d31 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 27 Nov 2023 13:29:46 +0000 Subject: [PATCH 3/4] fix tests --- tests/command_palette/test_declare_sources.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/command_palette/test_declare_sources.py b/tests/command_palette/test_declare_sources.py index fd411d4c5e..0c011bb355 100644 --- a/tests/command_palette/test_declare_sources.py +++ b/tests/command_palette/test_declare_sources.py @@ -1,3 +1,4 @@ +from textual._system_commands import SystemCommands from textual.app import App from textual.command import CommandPalette, Hit, Hits, Provider from textual.screen import Screen @@ -29,7 +30,7 @@ async def test_no_app_command_sources() -> None: """An app with no sources declared should work fine.""" async with AppWithNoSources().run_test() as pilot: assert isinstance(pilot.app.screen, CommandPalette) - assert pilot.app.screen._provider_classes == App.COMMANDS + assert pilot.app.screen._provider_classes == {SystemCommands} class AppWithSources(AppWithActiveCommandPalette): @@ -40,7 +41,7 @@ async def test_app_command_sources() -> None: """Command sources declared on an app should be in the command palette.""" async with AppWithSources().run_test() as pilot: assert isinstance(pilot.app.screen, CommandPalette) - assert pilot.app.screen._provider_classes == AppWithSources.COMMANDS + assert pilot.app.screen._provider_classes == {ExampleCommandSource} class AppWithInitialScreen(App[None]): @@ -61,7 +62,7 @@ async def test_no_screen_command_sources() -> None: """An app with a screen with no sources declared should work fine.""" async with AppWithInitialScreen(ScreenWithNoSources()).run_test() as pilot: assert isinstance(pilot.app.screen, CommandPalette) - assert pilot.app.screen._provider_classes == App.COMMANDS + assert pilot.app.screen._provider_classes == {SystemCommands} class ScreenWithSources(ScreenWithNoSources): From 748a7a93a3b72cf8d2221f509818d9f6bc99e939 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 27 Nov 2023 13:35:57 +0000 Subject: [PATCH 4/4] test fix --- tests/command_palette/test_declare_sources.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/command_palette/test_declare_sources.py b/tests/command_palette/test_declare_sources.py index 0c011bb355..e547fc4eb1 100644 --- a/tests/command_palette/test_declare_sources.py +++ b/tests/command_palette/test_declare_sources.py @@ -73,10 +73,10 @@ async def test_screen_command_sources() -> None: """Command sources declared on a screen should be in the command palette.""" async with AppWithInitialScreen(ScreenWithSources()).run_test() as pilot: assert isinstance(pilot.app.screen, CommandPalette) - assert ( - pilot.app.screen._provider_classes - == App.COMMANDS | ScreenWithSources.COMMANDS - ) + assert pilot.app.screen._provider_classes == { + SystemCommands, + ExampleCommandSource, + } class AnotherCommandSource(ExampleCommandSource):