Use workers to get suggestions.

This commit is contained in:
Rodrigo Girão Serrão
2023-05-23 15:16:24 +01:00
parent e63ec577cd
commit 239e5eebc6
2 changed files with 52 additions and 24 deletions

View File

@@ -1,35 +1,43 @@
from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass 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 from .message import Message
_SuggestionRequester = TypeVar("_SuggestionRequester", bound=MessageTarget)
"""Type variable for the message target that will request suggestions."""
@dataclass @dataclass
class SuggestionReady(Message): class SuggestionReady(Message):
"""Sent when a completion suggestion is ready.""" """Sent when a completion suggestion is ready."""
input_value: str initial_value: str
"""The input value that the suggestion was for.""" """The value to which the suggestion is for."""
suggestion: str suggestion: str
"""The string suggestion.""" """The string suggestion."""
class Suggester(ABC, Generic[_SuggestionRequester]): class Suggester(ABC):
"""Defines how [inputs][textual.widgets.Input] generate completion suggestions. """Defines how widgets generate completion suggestions.
To define a custom suggester, subclass `Suggester` and implement the async method To define a custom suggester, subclass `Suggester` and implement the async method
`get_suggestion`. `get_suggestion`.
See [`SuggestFromList`][textual.suggester.SuggestFromList] for an example. See [`SuggestFromList`][textual.suggester.SuggestFromList] for an example.
""" """
async def _get_suggestion( cache: dict[str, str | None] | None
self, requester: _SuggestionRequester, value: str """Suggestion cache, if used."""
) -> None:
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. """Used by widgets to get completion suggestions.
Note: Note:
@@ -40,20 +48,29 @@ class Suggester(ABC, Generic[_SuggestionRequester]):
requester: The message target that requested a suggestion. requester: The message target that requested a suggestion.
value: The current input value to complete. value: The current input value to complete.
""" """
if self.cache is None or value not in self.cache:
suggestion = await self.get_suggestion(value) 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: if suggestion is None:
return return
requester.post_message(SuggestionReady(value, suggestion)) requester.post_message(SuggestionReady(value, suggestion))
@abstractmethod @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. """Try to get a completion suggestion for the given input value.
Custom suggesters should implement this method. Custom suggesters should implement this method.
Note:
If your implementation is not deterministic, you may need to disable caching.
Args: Args:
value: The current value of the input widget. value: The current value of the requester widget.
Returns: Returns:
A valid suggestion or `None`. A valid suggestion or `None`.
@@ -61,8 +78,18 @@ class Suggester(ABC, Generic[_SuggestionRequester]):
pass pass
class SuggestFromList(Suggester[_SuggestionRequester]): class SuggestFromList(Suggester):
"""Give completion suggestions based on a fixed list of options.""" """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: def __init__(self, suggestions: Iterable[str]) -> None:
"""Creates a suggester based off of a given iterable of possibilities. """Creates a suggester based off of a given iterable of possibilities.
@@ -70,9 +97,10 @@ class SuggestFromList(Suggester[_SuggestionRequester]):
Args: Args:
suggestions: Valid suggestions sorted by decreasing priority. 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. """Gets a completion from the given possibilities.
Args: Args:
@@ -81,7 +109,7 @@ class SuggestFromList(Suggester[_SuggestionRequester]):
Returns: Returns:
A valid completion suggestion or `None`. A valid completion suggestion or `None`.
""" """
for suggestion in self.suggestions: for suggestion in self._suggestions:
if suggestion.startswith(value): if suggestion.startswith(value):
return suggestion return suggestion
return None return None

View File

@@ -156,7 +156,7 @@ class Input(Widget, can_focus=True):
_cursor_visible = reactive(True) _cursor_visible = reactive(True)
password = reactive(False) password = reactive(False)
max_size: reactive[int | None] = reactive(None) max_size: reactive[int | None] = reactive(None)
suggester: Suggester[Input] | None suggester: Suggester | None
"""The suggester used to provide completions as the user types.""" """The suggester used to provide completions as the user types."""
_suggestion = reactive("") _suggestion = reactive("")
"""A completion suggestion for the current value in the input.""" """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: async def watch_value(self, value: str) -> None:
self._suggestion = "" self._suggestion = ""
if self.suggester and value: 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: if self.styles.auto_dimensions:
self.refresh(layout=True) self.refresh(layout=True)
self.post_message(self.Changed(self, value)) 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: async def _on_suggestion_ready(self, event: SuggestionReady) -> None:
"""Handle suggestion messages and set the suggestion when relevant.""" """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 self._suggestion = event.suggestion
def insert_text_at_cursor(self, text: str) -> None: def insert_text_at_cursor(self, text: str) -> None: