From 914e50a70ff93d35ed7eccc05335a563ae6f1ece 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, 2 May 2023 15:12:53 +0100 Subject: [PATCH] Export types & doc improvements (#2329) * Export types used in app.py * Export more linked types/errors/classes. * Remove custom template. * Address review comments. We need to have explicit 'Returns:' sections in properties if we want to link to the return type while https://github.com/mkdocstrings/python/issues/65 is open. * Improve docs. --- .../_templates/python/material/attribute.html | 67 ------------------- docs/api/errors.md | 1 + docs/api/filter.md | 1 + docs/api/scrollbar.md | 1 + docs/api/types.md | 1 + mkdocs-nav.yml | 4 ++ src/textual/_animator.py | 10 +++ src/textual/_context.py | 2 +- src/textual/_types.py | 4 ++ src/textual/app.py | 64 +++++++++++------- src/textual/color.py | 5 +- src/textual/dom.py | 4 ++ src/textual/errors.py | 6 +- src/textual/geometry.py | 3 +- src/textual/message_pump.py | 6 +- src/textual/screen.py | 3 + src/textual/scrollbar.py | 7 +- src/textual/timer.py | 1 + src/textual/types.py | 20 ++++++ src/textual/worker.py | 1 + 20 files changed, 108 insertions(+), 103 deletions(-) delete mode 100644 docs/_templates/python/material/attribute.html create mode 100644 docs/api/errors.md create mode 100644 docs/api/filter.md create mode 100644 docs/api/scrollbar.md create mode 100644 docs/api/types.md create mode 100644 src/textual/types.py diff --git a/docs/_templates/python/material/attribute.html b/docs/_templates/python/material/attribute.html deleted file mode 100644 index b4f6bcaf8..000000000 --- a/docs/_templates/python/material/attribute.html +++ /dev/null @@ -1,67 +0,0 @@ -{{ log.debug("Rendering " + attribute.path) }} - -
-{% with html_id = attribute.path %} - - {% if root %} - {% set show_full_path = config.show_root_full_path %} - {% set root_members = True %} - {% elif root_members %} - {% set show_full_path = config.show_root_members_full_path or config.show_object_full_path %} - {% set root_members = False %} - {% else %} - {% set show_full_path = config.show_object_full_path %} - {% endif %} - - {% if not root or config.show_root_heading %} - - {% filter heading(heading_level, - role="data" if attribute.parent.kind.value == "module" else "attr", - id=html_id, - class="doc doc-heading", - toc_label=attribute.name) %} - - {% if config.separate_signature %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} - {% else %} - {% filter highlight(language="python", inline=True) %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} - {% if attribute.annotation %}: {{ attribute.annotation }}{% endif %} - {% endfilter %} - {% endif %} - - {% with labels = attribute.labels %} - {% include "labels.html" with context %} - {% endwith %} - - {% endfilter %} - - {% if config.separate_signature %} - {% filter highlight(language="python", inline=False) %} - {% filter format_code(config.line_length) %} - {% if show_full_path %}{{ attribute.path }}{% else %}{{ attribute.name }}{% endif %} - {% if attribute.annotation %}: {{ attribute.annotation|safe }}{% endif %} - {% endfilter %} - {% endfilter %} - {% endif %} - - {% else %} - {% if config.show_root_toc_entry %} - {% filter heading(heading_level, - role="data" if attribute.parent.kind.value == "module" else "attr", - id=html_id, - toc_label=attribute.path if config.show_root_full_path else attribute.name, - hidden=True) %} - {% endfilter %} - {% endif %} - {% set heading_level = heading_level - 1 %} - {% endif %} - -
- {% with docstring_sections = attribute.docstring.parsed %} - {% include "docstring.html" with context %} - {% endwith %} -
- -{% endwith %} -
diff --git a/docs/api/errors.md b/docs/api/errors.md new file mode 100644 index 000000000..5ee969dd8 --- /dev/null +++ b/docs/api/errors.md @@ -0,0 +1 @@ +::: textual.errors diff --git a/docs/api/filter.md b/docs/api/filter.md new file mode 100644 index 000000000..37b6966b9 --- /dev/null +++ b/docs/api/filter.md @@ -0,0 +1 @@ +::: textual.filter diff --git a/docs/api/scrollbar.md b/docs/api/scrollbar.md new file mode 100644 index 000000000..8945dcd89 --- /dev/null +++ b/docs/api/scrollbar.md @@ -0,0 +1 @@ +::: textual.scrollbar diff --git a/docs/api/types.md b/docs/api/types.md new file mode 100644 index 000000000..8d89c89a6 --- /dev/null +++ b/docs/api/types.md @@ -0,0 +1 @@ +::: textual.types diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index ad19aa1e9..3a89dd15f 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -164,6 +164,8 @@ nav: - "api/coordinate.md" - "api/dom_node.md" - "api/events.md" + - "api/errors.md" + - "api/filter.md" - "api/geometry.md" - "api/index.md" - "api/logger.md" @@ -175,9 +177,11 @@ nav: - "api/query.md" - "api/reactive.md" - "api/screen.md" + - "api/scrollbar.md" - "api/scroll_view.md" - "api/strip.md" - "api/timer.md" + - "api/types.md" - "api/walk.md" - "api/widget.md" - "api/work.md" diff --git a/src/textual/_animator.py b/src/textual/_animator.py index e213e25eb..a72043f4d 100644 --- a/src/textual/_animator.py +++ b/src/textual/_animator.py @@ -21,6 +21,10 @@ if TYPE_CHECKING: """Animation keys are the id of the object and the attribute being animated.""" EasingFunction = Callable[[float], float] +"""Signature for a function that parametrises animation speed. + +An easing function must map the interval [0, 1] into the interval [0, 1]. +""" class AnimationError(Exception): @@ -32,6 +36,12 @@ ReturnType = TypeVar("ReturnType") @runtime_checkable class Animatable(Protocol): + """Protocol for objects that can have their intrinsic values animated. + + For example, the transition between two colors can be animated + because the class [`Color`][textual.color.Color.blend] satisfies this protocol. + """ + def blend( self: ReturnType, destination: ReturnType, factor: float ) -> ReturnType: # pragma: no cover diff --git a/src/textual/_context.py b/src/textual/_context.py index aedc4d3d5..87b6f8443 100644 --- a/src/textual/_context.py +++ b/src/textual/_context.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: class NoActiveAppError(RuntimeError): - pass + """Runtime error raised if we try to retrieve the active app when there is none.""" active_app: ContextVar["App"] = ContextVar("active_app") diff --git a/src/textual/_types.py b/src/textual/_types.py index 4b539c53a..48e38e006 100644 --- a/src/textual/_types.py +++ b/src/textual/_types.py @@ -8,6 +8,8 @@ if TYPE_CHECKING: class MessageTarget(Protocol): + """Protocol that must be followed by objects that can receive messages.""" + async def _post_message(self, message: "Message") -> bool: ... @@ -25,6 +27,7 @@ class EventTarget(Protocol): SegmentLines = List[List["Segment"]] CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]] +"""Type used for arbitrary callables used in callbacks.""" WatchCallbackType = Union[ Callable[[], Awaitable[None]], Callable[[Any], Awaitable[None]], @@ -33,3 +36,4 @@ WatchCallbackType = Union[ Callable[[Any], None], Callable[[Any, Any], None], ] +"""Type used for callbacks passed to the `watch` method of widgets.""" diff --git a/src/textual/app.py b/src/textual/app.py index 7f4c6dd25..896272cd1 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -97,8 +97,11 @@ from .widget import AwaitMount, Widget if TYPE_CHECKING: from typing_extensions import Coroutine, TypeAlias + # Unused & ignored imports are needed for the docs to link to these objects: + from .css.query import WrongType # type: ignore # noqa: F401 from .devtools.client import DevtoolsClient from .pilot import Pilot + from .widget import MountError # type: ignore # noqa: F401 PLATFORM = platform.system() WINDOWS = PLATFORM == "Windows" @@ -137,6 +140,7 @@ ComposeResult = Iterable[Widget] RenderResult = RenderableType AutopilotCallbackType: TypeAlias = "Callable[[Pilot], Coroutine[Any, Any, None]]" +"""Signature for valid callbacks that can be used to control apps.""" class AppError(Exception): @@ -167,6 +171,7 @@ CSSPathType = Union[ PurePath, List[Union[str, PurePath]], ] +"""Valid ways of specifying paths to CSS files.""" CallThreadReturnType = TypeVar("CallThreadReturnType") @@ -186,17 +191,7 @@ class _NullFile: @rich.repr.auto class App(Generic[ReturnType], DOMNode): - """The base class for Textual Applications. - - Args: - driver_class: Driver class or `None` to auto-detect. This will be used by some Textual tools. - css_path: Path to CSS or `None` to use the `CSS_PATH` class variable. - To load multiple CSS files, pass a list of strings or paths which will be loaded in order. - watch_css: Reload CSS if the files changed. This is set automatically if you are using `textual run` with the `dev` switch. - - Raises: - CssPathError: When the supplied CSS path(s) are an unexpected type. - """ + """The base class for Textual Applications.""" CSS: ClassVar[str] = "" """Inline CSS, useful for quick scripts. This is loaded after CSS_PATH, @@ -218,8 +213,10 @@ class App(Generic[ReturnType], DOMNode): """ SCREENS: ClassVar[dict[str, Screen | Callable[[], Screen]]] = {} + """Screens associated with the app for the lifetime of the app.""" _BASE_PATH: str | None = None CSS_PATH: ClassVar[CSSPathType | None] = None + """File paths to load CSS from.""" TITLE: str | None = None """A class variable to set the *default* title for the application. @@ -255,6 +252,20 @@ class App(Generic[ReturnType], DOMNode): css_path: CSSPathType | None = None, watch_css: bool = False, ): + """Create an instance of an app. + + Args: + driver_class: Driver class or `None` to auto-detect. + This will be used by some Textual tools. + css_path: Path to CSS or `None` to use the `CSS_PATH` class variable. + To load multiple CSS files, pass a list of strings or paths which + will be loaded in order. + watch_css: Reload CSS if the files changed. This is set automatically if + you are using `textual run` with the `dev` switch. + + Raises: + CssPathError: When the supplied CSS path(s) are an unexpected type. + """ super().__init__() self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", "")) @@ -406,7 +417,7 @@ class App(Generic[ReturnType], DOMNode): @property def return_value(self) -> ReturnType | None: - """The return value of the app, or `None` if it as not yet been set. + """The return value of the app, or `None` if it has not yet been set. The return value is set when calling [exit][textual.app.App.exit]. """ @@ -414,10 +425,11 @@ class App(Generic[ReturnType], DOMNode): @property def children(self) -> Sequence["Widget"]: - """A view on to the App's children. + """A view onto the app's immediate children. This attribute exists on all widgets. - In the case of the App, it will only every contain a single child, which will be the currently active screen. + In the case of the App, it will only ever contain a single child, which will + be the currently active screen. Returns: A sequence of widgets. @@ -499,7 +511,7 @@ class App(Generic[ReturnType], DOMNode): @property def screen_stack(self) -> Sequence[Screen]: - """The current screen stack. + """A snapshot of the current screen stack. Returns: A snapshot of the current state of the screen stack. @@ -523,7 +535,7 @@ class App(Generic[ReturnType], DOMNode): @property def focused(self) -> Widget | None: - """The widget that is focused on the currently active screen. + """The widget that is focused on the currently active screen, or `None`. Focused widgets receive keyboard input. @@ -534,7 +546,7 @@ class App(Generic[ReturnType], DOMNode): @property def namespace_bindings(self) -> dict[str, tuple[DOMNode, Binding]]: - """Get current bindings. + """Get currently active bindings. If no widget is focused, then app-level bindings are returned. If a widget is focused, then any bindings present in the active screen and app are merged and returned. @@ -542,8 +554,7 @@ class App(Generic[ReturnType], DOMNode): This property may be used to inspect current bindings. Returns: - - A mapping of keys on to node + binding. + A mapping of keys onto pairs of nodes and bindings. """ namespace_binding_map: dict[str, tuple[DOMNode, Binding]] = {} @@ -650,7 +661,7 @@ class App(Generic[ReturnType], DOMNode): @property def screen(self) -> Screen: - """Screen: The current screen. + """The current active screen. Returns: The currently active (visible) screen. @@ -689,7 +700,7 @@ class App(Generic[ReturnType], DOMNode): @property def log(self) -> Logger: - """Textual log interface. + """The textual logger. Example: ```python @@ -1162,14 +1173,15 @@ class App(Generic[ReturnType], DOMNode): Args: id: The ID of the node to search for. - expect_type: Require the object be of the supplied type, or None for any type. + expect_type: Require the object be of the supplied type, + or use `None` to apply no type restriction. Returns: The first child of this node with the specified ID. Raises: - NoMatches: if no children could be found for this ID - WrongType: if the wrong type was found. + NoMatches: If no children could be found for this ID. + WrongType: If the wrong type was found. """ return ( self.screen.get_child_by_id(id) @@ -1463,7 +1475,6 @@ class App(Generic[ReturnType], DOMNode): with [install_screen][textual.app.App.install_screen]. Textual will also uninstall screens automatically on exit. - Args: screen: The screen to uninstall or the name of a installed screen. @@ -1836,6 +1847,7 @@ class App(Generic[ReturnType], DOMNode): *widgets: The widget(s) to register. before: A location to mount before. after: A location to mount after. + Returns: List of modified widgets. """ @@ -2140,7 +2152,7 @@ class App(Generic[ReturnType], DOMNode): or None to use app. Returns: - True if the event has handled. + True if the event has been handled. """ if isinstance(action, str): target, params = actions.parse(action) diff --git a/src/textual/color.py b/src/textual/color.py index a3fca9282..12a7f3c65 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -536,7 +536,7 @@ class Color(NamedTuple): return self.darken(-amount, alpha) @lru_cache(maxsize=1024) - def get_contrast_text(self, alpha=0.95) -> Color: + def get_contrast_text(self, alpha: float = 0.95) -> Color: """Get a light or dark color that best contrasts this color, for use with text. Args: @@ -576,9 +576,8 @@ class Gradient: Positions that are between stops will return a blended color. - Args: - factor: A number between 0 and 1, where 0 is the first stop, and 1 is the last. + position: A number between 0 and 1, where 0 is the first stop, and 1 is the last. Returns: A color. diff --git a/src/textual/dom.py b/src/textual/dom.py index d292bc38d..475ed1c55 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -54,12 +54,16 @@ if TYPE_CHECKING: from .worker import Worker, WorkType, ResultType from typing_extensions import Self, TypeAlias + # Unused & ignored imports are needed for the docs to link to these objects: + from .css.query import NoMatches, TooManyMatches, WrongType # type: ignore # noqa: F401 + from typing_extensions import Literal _re_identifier = re.compile(IDENTIFIER) WalkMethod: TypeAlias = Literal["depth", "breadth"] +"""Valid walking methods for the [`DOMNode.walk_children` method][textual.dom.DOMNode.walk_children].""" class BadIdentifier(Exception): diff --git a/src/textual/errors.py b/src/textual/errors.py index 032c3ed3b..021bcff0f 100644 --- a/src/textual/errors.py +++ b/src/textual/errors.py @@ -14,4 +14,8 @@ class RenderError(TextualError): class DuplicateKeyHandlers(TextualError): - """More than one handler for a single key press. E.g. key_ctrl_i and key_tab handlers both found on one object.""" + """More than one handler for a single key press. + + For example, if the handlers `key_ctrl_i` and `key_tab` were defined on the same + widget, then this error would be raised. + """ diff --git a/src/textual/geometry.py b/src/textual/geometry.py index fb1595815..d111cb4aa 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -27,6 +27,7 @@ if TYPE_CHECKING: SpacingDimensions: TypeAlias = Union[ int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int] ] +"""The valid ways in which you can specify spacing.""" T = TypeVar("T", int, float) @@ -1043,4 +1044,4 @@ class Spacing(NamedTuple): NULL_OFFSET: Final = Offset(0, 0) -"""An Offset constant for (0, 0).""" +"""An [offset][textual.geometry.Offset] constant for (0, 0).""" diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4a3b1a41c..101446c37 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -337,7 +337,7 @@ class MessagePump(metaclass=_MessagePumpMeta): self._timers.add(timer) return timer - def call_after_refresh(self, callback: Callable, *args, **kwargs) -> None: + def call_after_refresh(self, callback: Callable, *args: Any, **kwargs: Any) -> None: """Schedule a callback to run after all messages are processed and the screen has been refreshed. Positional and keyword arguments are passed to the callable. @@ -350,7 +350,7 @@ class MessagePump(metaclass=_MessagePumpMeta): message = messages.InvokeLater(partial(callback, *args, **kwargs)) self.post_message(message) - def call_later(self, callback: Callable, *args, **kwargs) -> None: + def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> None: """Schedule a callback to run after all messages are processed in this object. Positional and keywords arguments are passed to the callable. @@ -362,7 +362,7 @@ class MessagePump(metaclass=_MessagePumpMeta): message = events.Callback(callback=partial(callback, *args, **kwargs)) self.post_message(message) - def call_next(self, callback: Callable, *args, **kwargs) -> None: + def call_next(self, callback: Callable, *args: Any, **kwargs: Any) -> None: """Schedule a callback to run immediately after processing the current message. Args: diff --git a/src/textual/screen.py b/src/textual/screen.py index 8f9528c56..dac819314 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -41,6 +41,9 @@ from .widget import Widget if TYPE_CHECKING: from typing_extensions import Final + # Unused & ignored imports are needed for the docs to link to these objects: + from .errors import NoWidget # type: ignore # noqa: F401 + # Screen updates will be batched so that they don't happen more often than 60 times per second: UPDATE_PERIOD: Final[float] = 1 / 60 diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index 1d590e2ed..1a9f35b15 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -1,3 +1,8 @@ +""" +Implements the scrollbar-related widgets for internal use. + +You will not need to use the widgets defined in this module. +""" from __future__ import annotations from math import ceil @@ -18,7 +23,7 @@ from .widget import Widget class ScrollMessage(Message, bubble=False): - pass + """Base class for all scrollbar messages.""" @rich.repr.auto diff --git a/src/textual/timer.py b/src/textual/timer.py index 96962aacc..42e6b9789 100644 --- a/src/textual/timer.py +++ b/src/textual/timer.py @@ -20,6 +20,7 @@ from ._time import sleep from ._types import MessageTarget TimerCallback = Union[Callable[[], Awaitable[None]], Callable[[], None]] +"""Type of valid callbacks to be used with timers.""" class EventTargetGone(Exception): diff --git a/src/textual/types.py b/src/textual/types.py new file mode 100644 index 000000000..dd681f657 --- /dev/null +++ b/src/textual/types.py @@ -0,0 +1,20 @@ +""" +Export some objects that are used by Textual and that help document other features. +""" + +from ._animator import Animatable, EasingFunction +from ._context import NoActiveAppError +from ._types import CallbackType, MessageTarget, WatchCallbackType +from .actions import ActionParseResult +from .css.styles import RenderStyles + +__all__ = [ + "ActionParseResult", + "Animatable", + "CallbackType", + "EasingFunction", + "MessageTarget", + "NoActiveAppError", + "RenderStyles", + "WatchCallbackType", +] diff --git a/src/textual/worker.py b/src/textual/worker.py index 7d3e2e912..6f5ac63bd 100644 --- a/src/textual/worker.py +++ b/src/textual/worker.py @@ -99,6 +99,7 @@ WorkType: TypeAlias = Union[ Callable[[], ResultType], Awaitable[ResultType], ] +"""Type used for [workers](/guide/workers/).""" class _ReprText: