From 239e5eebc6a1a41db9daa462ae7ac5d2d13e86e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 23 May 2023 15:16:24 +0100 Subject: [PATCH] Use workers to get suggestions. --- src/textual/suggester.py | 70 ++++++++++++++++++++++++----------- src/textual/widgets/_input.py | 6 +-- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/textual/suggester.py b/src/textual/suggester.py index 8fe0da029..b290b1804 100644 --- a/src/textual/suggester.py +++ b/src/textual/suggester.py @@ -1,35 +1,43 @@ +from __future__ import annotations + from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Generic, Iterable, Optional, TypeVar +from typing import Iterable -from ._types import MessageTarget +from .dom import DOMNode from .message import Message -_SuggestionRequester = TypeVar("_SuggestionRequester", bound=MessageTarget) -"""Type variable for the message target that will request suggestions.""" - @dataclass class SuggestionReady(Message): """Sent when a completion suggestion is ready.""" - input_value: str - """The input value that the suggestion was for.""" + initial_value: str + """The value to which the suggestion is for.""" suggestion: str """The string suggestion.""" -class Suggester(ABC, Generic[_SuggestionRequester]): - """Defines how [inputs][textual.widgets.Input] generate completion suggestions. +class Suggester(ABC): + """Defines how widgets generate completion suggestions. To define a custom suggester, subclass `Suggester` and implement the async method `get_suggestion`. See [`SuggestFromList`][textual.suggester.SuggestFromList] for an example. """ - async def _get_suggestion( - self, requester: _SuggestionRequester, value: str - ) -> None: + cache: dict[str, str | None] | None + """Suggestion cache, if used.""" + + def __init__(self, use_cache: bool = True): + """Create a suggester object. + + Args: + use_cache: Whether to cache suggestion results. + """ + self.cache = {} if use_cache else None + + async def _get_suggestion(self, requester: DOMNode, value: str) -> None: """Used by widgets to get completion suggestions. Note: @@ -40,20 +48,29 @@ class Suggester(ABC, Generic[_SuggestionRequester]): requester: The message target that requested a suggestion. value: The current input value to complete. """ - suggestion = await self.get_suggestion(value) + + if self.cache is None or value not in self.cache: + suggestion = await self.get_suggestion(value) + if self.cache is not None: + self.cache[value] = suggestion + else: + suggestion = self.cache[value] + if suggestion is None: return - requester.post_message(SuggestionReady(value, suggestion)) @abstractmethod - async def get_suggestion(self, value: str) -> Optional[str]: + async def get_suggestion(self, value: str) -> str | None: """Try to get a completion suggestion for the given input value. Custom suggesters should implement this method. + Note: + If your implementation is not deterministic, you may need to disable caching. + Args: - value: The current value of the input widget. + value: The current value of the requester widget. Returns: A valid suggestion or `None`. @@ -61,8 +78,18 @@ class Suggester(ABC, Generic[_SuggestionRequester]): pass -class SuggestFromList(Suggester[_SuggestionRequester]): - """Give completion suggestions based on a fixed list of options.""" +class SuggestFromList(Suggester): + """Give completion suggestions based on a fixed list of options. + + Example: + ```py + countries = ["England", "Scotland", "Portugal", "Spain", "France"] + + class MyApp(App[None]): + def compose(self) -> ComposeResult: + yield Input(suggester=SuggestFromList(countries)) + ``` + """ def __init__(self, suggestions: Iterable[str]) -> None: """Creates a suggester based off of a given iterable of possibilities. @@ -70,9 +97,10 @@ class SuggestFromList(Suggester[_SuggestionRequester]): Args: suggestions: Valid suggestions sorted by decreasing priority. """ - self.suggestions = list(suggestions) + super().__init__() + self._suggestions = list(suggestions) - async def get_suggestion(self, value: str) -> Optional[str]: + async def get_suggestion(self, value: str) -> str | None: """Gets a completion from the given possibilities. Args: @@ -81,7 +109,7 @@ class SuggestFromList(Suggester[_SuggestionRequester]): Returns: A valid completion suggestion or `None`. """ - for suggestion in self.suggestions: + for suggestion in self._suggestions: if suggestion.startswith(value): return suggestion return None diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index d87bb47da..c5be91da6 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -156,7 +156,7 @@ class Input(Widget, can_focus=True): _cursor_visible = reactive(True) password = reactive(False) max_size: reactive[int | None] = reactive(None) - suggester: Suggester[Input] | None + suggester: Suggester | None """The suggester used to provide completions as the user types.""" _suggestion = reactive("") """A completion suggestion for the current value in the input.""" @@ -284,7 +284,7 @@ class Input(Widget, can_focus=True): async def watch_value(self, value: str) -> None: self._suggestion = "" if self.suggester and value: - self.call_next(self.suggester._get_suggestion, self, value) + self.run_worker(self.suggester._get_suggestion(self, value)) if self.styles.auto_dimensions: self.refresh(layout=True) self.post_message(self.Changed(self, value)) @@ -388,7 +388,7 @@ class Input(Widget, can_focus=True): async def _on_suggestion_ready(self, event: SuggestionReady) -> None: """Handle suggestion messages and set the suggestion when relevant.""" - if event.input_value == self.value: + if event.initial_value == self.value: self._suggestion = event.suggestion def insert_text_at_cursor(self, text: str) -> None: