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: