Repository: Textualize/textual
Pull request: Export types & doc improvements
This will close #2168 when it's done.

I did a pass on the App API page and looked for types that are used in signatures and that aren't documented.
This is the type of change I suggest.

If everyone is ok with this type of changes, I will go over the remainder of the API pages.

We still need to figure out how to document type aliases without duplicating their definition.

Problem statement:
Document custom types used and actually show their definition
I think we should document, perhaps in the API page, the custom types we use for things that are already documented.

For example, `App.CSS_PATH` shows up in the docs and is typed as `CSSPathType`, and the type itself is pretty much all the documentation needed for `App.CSS_PATH`.
However, `CSSPathType` isn't documented.

In my opinion, we chould/should document these custom types in the module they are defined in, or they should all be collected in a “Types” API page, for example.

**Note**: I spent some time trying to do this for a single type (`CSSPathType`) and I managed to put it in a separate API page.
However, I wasn't getting two important things:
 1. The signature of `App.CSS_PATH` wasn't linking to `CSSPathType`.
 2. I didn't manage to have `CSSPathType` show its signature, which is what I really wanted to have documented.

Hints from issue discussion:
I think the most straightforward solution would be to cover it in the docstrings. We do seem to be missing docstrings for `CSS_PATH`.
So, to clarify, you are suggesting that the docstring for `CSS_PATH` mentions and links to `CSSPathType`, for example?
Yeah, if we can't somehow expand the `CSSPathType` then we could just say it can be a string, list of strings, or None.
Ok. I'll see what I can do.
Ok, I can correctly export useful type aliases and I can have the docs cross-reference them automatically when they show up in signatures. (Thanks, Dave, for the work in #2287!)

However, if you show a type alias in the docs, it won't expand its definition, even if you set the option `show_source: true`.
Sadly, this is probably the most important step of all!

So, do I manually add this duplicated info to the docstrings of those type aliases?
Do we create a small mkdocs/mkdocstrings plugin that handles this?
What happens if we use the type in the docstring? i.e. something like this?

```python
CSS_PATH = ""
"""str | list[str] | None: Path to css files, or a list of paths."""
```
That's fine and it'll show up but it's duplicated...
And it may look extra confusing when you add the missing typing to `CSS_PATH` which will also show up in the docs:

```py
CSS_PATH: CSSPathType = ""
"""str | list[str] | None: Path to css files, or a list of paths."""
```

It wouldn't be _terrible_ if we added it to the type alias:

```py
CSSPathType = Union[
    str,
    PurePath,
    List[Union[str, PurePath]],
]
"""str | PurePath | List[str | PurePath]:  Valid types to set the path to CSS files."""
```

it is still duplication, but it is less duplication for type aliases that are used more than once.
I'll see what reply I get back from https://github.com/mkdocstrings/mkdocstrings/issues/559

Commit message: Export more linked types/errors/classes.

Patch to review:
diff --git a/docs/api/errors.md b/docs/api/errors.md
new file mode 100644
index 0000000000..5ee969dd88
--- /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 0000000000..37b6966b93
--- /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 0000000000..8945dcd895
--- /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 0000000000..8d89c89a6a
--- /dev/null
+++ b/docs/api/types.md
@@ -0,0 +1 @@
+::: textual.types
diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml
index 83c41023e8..7e234fad4e 100644
--- a/mkdocs-nav.yml
+++ b/mkdocs-nav.yml
@@ -163,6 +163,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"
@@ -174,9 +176,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 8c7572446a..e56ccb84c6 100644
--- a/src/textual/_animator.py
+++ b/src/textual/_animator.py
@@ -21,6 +21,10 @@
     """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 @@ class AnimationError(Exception):
 
 @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 aedc4d3d51..87b6f8443d 100644
--- a/src/textual/_context.py
+++ b/src/textual/_context.py
@@ -11,7 +11,7 @@
 
 
 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 4b539c53a6..48e38e0069 100644
--- a/src/textual/_types.py
+++ b/src/textual/_types.py
@@ -8,6 +8,8 @@
 
 
 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 @@ def post_message(self, message: "Message") -> bool:
 
 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 @@ def post_message(self, message: "Message") -> bool:
     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 5efe4e3805..1675cc10f7 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -98,8 +98,11 @@
 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"
@@ -138,6 +141,7 @@
 RenderResult = RenderableType
 
 AutopilotCallbackType: TypeAlias = "Callable[[Pilot], Coroutine[Any, Any, None]]"
+"""Signature for valid callbacks that can be used to control apps."""
 
 
 class AppError(Exception):
@@ -168,6 +172,7 @@ class CssPathError(Exception):
     PurePath,
     List[Union[str, PurePath]],
 ]
+"""Valid ways of specifying paths to CSS files."""
 
 CallThreadReturnType = TypeVar("CallThreadReturnType")
 
@@ -187,17 +192,7 @@ def isatty(self) -> bool:
 
 @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,
@@ -219,8 +214,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.
@@ -248,7 +245,6 @@ class App(Generic[ReturnType], DOMNode):
         ```python
         self.app.dark = not self.app.dark  # Toggle dark mode
         ```
-
     """
 
     def __init__(
@@ -257,6 +253,20 @@ def __init__(
         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", ""))
 
@@ -399,33 +409,24 @@ def __init__(
 
     @property
     def workers(self) -> WorkerManager:
-        """The [worker](guide/workers/) manager.
-
-        Returns:
-            An object to manage workers.
-
-        """
+        """The [worker](guide/workers/) manager."""
         return self._workers
 
     @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].
-
         """
         return self._return_value
 
     @property
     def children(self) -> Sequence["Widget"]:
-        """A view on to the App's children.
+        """A view onto the app's direct 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.
-
-        Returns:
-            A sequence of widgets.
-
+        In the case of the App, it will only ever contain a single child, which will
+        be the currently active screen.
         """
         try:
             return (self.screen,)
@@ -477,7 +478,6 @@ def animate(
             delay: A delay (in seconds) before the animation starts.
             easing: An easing method.
             on_complete: A callable to invoke when the animation is finished.
-
         """
         self._animate(
             attribute,
@@ -500,18 +500,12 @@ def is_headless(self) -> bool:
         """Is the driver running in 'headless' mode?
 
         Headless mode is used when running tests with [run_test][textual.app.App.run_test].
-
         """
         return False if self._driver is None else self._driver.is_headless
 
     @property
     def screen_stack(self) -> Sequence[Screen]:
-        """The current screen stack.
-
-        Returns:
-            A snapshot of the current state of the screen stack.
-
-        """
+        """A snapshot of the current screen stack."""
         return self._screen_stack.copy()
 
     def exit(
@@ -531,29 +525,20 @@ def exit(
 
     @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.
-
-        Returns:
-            The currently focused widget, or `None` if nothing is focused.
-
         """
         return self.screen.focused
 
     @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.
 
         This property may be used to inspect current bindings.
-
-        Returns:
-
-            A mapping of keys on to node + binding.
-
         """
 
         namespace_binding_map: dict[str, tuple[DOMNode, Binding]] = {}
@@ -571,7 +556,6 @@ def compose(self) -> ComposeResult:
         """Yield child widgets for a container.
 
         This method should be implemented in a subclass.
-
         """
         yield from ()
 
@@ -591,7 +575,6 @@ def watch_dark(self, dark: bool) -> None:
 
         This method handles the transition between light and dark mode when you
         change the [dark][textual.app.App.dark] attribute.
-
         """
         self.set_class(dark, "-dark-mode")
         self.set_class(not dark, "-light-mode")
@@ -662,10 +645,7 @@ def animator(self) -> Animator:
 
     @property
     def screen(self) -> Screen:
-        """Screen: The current screen.
-
-        Returns:
-            The currently active (visible) screen.
+        """The current active screen.
 
         Raises:
             ScreenStackError: If there are no screens on the stack.
@@ -688,12 +668,7 @@ def _background_screens(self) -> list[Screen]:
 
     @property
     def size(self) -> Size:
-        """The size of the terminal.
-
-        Returns:
-            Size of the terminal.
-
-        """
+        """The size of the terminal."""
         if self._driver is not None and self._driver._size is not None:
             width, height = self._driver._size
         else:
@@ -702,17 +677,13 @@ def size(self) -> Size:
 
     @property
     def log(self) -> Logger:
-        """Textual log interface.
+        """The textual logger.
 
         Example:
             ```python
             self.log("Hello, World!")
             self.log(self.tree)
             ```
-
-        Returns:
-            A Textual logger.
-
         """
         return self._logger
 
@@ -837,7 +808,6 @@ def export_screenshot(self, *, title: str | None = None) -> str:
         Args:
             title: The title of the exported screenshot or None
                 to use app title.
-
         """
         assert self._driver is not None, "App must be running"
         width, height = self.size
@@ -985,8 +955,6 @@ async def run_test(
             headless: Run in headless mode (no output or input).
             size: Force terminal size to `(WIDTH, HEIGHT)`,
                 or None to auto-detect.
-
-
         """
         from .pilot import Pilot
 
@@ -1179,14 +1147,15 @@ def get_child_by_id(
 
         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)
@@ -1364,7 +1333,6 @@ def _get_screen(self, screen: Screen | str) -> tuple[Screen, AwaitMount]:
 
         Returns:
             A screen instance and an awaitable that awaits the children mounting.
-
         """
         _screen = self.get_screen(screen)
         if not _screen.is_running:
@@ -1385,7 +1353,6 @@ def _replace_screen(self, screen: Screen) -> Screen:
 
         Returns:
             The screen that was replaced.
-
         """
         if self._screen_stack:
             self.screen.refresh()
@@ -1432,7 +1399,6 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount:
 
         Args:
             screen: Either a Screen object or screen name (the `name` argument when installed).
-
         """
         if not isinstance(screen, (Screen, str)):
             raise TypeError(
@@ -1483,7 +1449,6 @@ def uninstall_screen(self, screen: Screen | str) -> str | None:
         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.
 
@@ -1770,7 +1735,6 @@ async def _ready(self) -> None:
         """Called immediately prior to processing messages.
 
         May be used as a hook for any operations that should run first.
-
         """
 
         async def take_screenshot() -> None:
@@ -1857,9 +1821,9 @@ def _register(
             *widgets: The widget(s) to register.
             before: A location to mount before.
             after: A location to mount after.
+
         Returns:
             List of modified widgets.
-
         """
 
         if not widgets:
@@ -2061,8 +2025,6 @@ def bell(self) -> None:
 
         For terminals that support a bell, this typically makes a notification or error sound.
         Some terminals may make no sound or display a visual bell indicator, depending on configuration.
-
-
         """
         if not self.is_headless and self._driver is not None:
             self._driver.write("\07")
@@ -2164,7 +2126,7 @@ async def run_action(
                 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)
@@ -2345,7 +2307,6 @@ def _walk_children(self, root: Widget) -> Iterable[list[Widget]]:
 
         Returns:
             The child widgets of root.
-
         """
         stack: list[Widget] = [root]
         pop = stack.pop
diff --git a/src/textual/color.py b/src/textual/color.py
index ca13075000..89d0fa1e3a 100644
--- a/src/textual/color.py
+++ b/src/textual/color.py
@@ -547,7 +547,7 @@ def lighten(self, amount: float, alpha: float | None = None) -> Color:
         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:
@@ -587,9 +587,8 @@ def get_color(self, position: float) -> Color:
 
         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 430ebeb433..97dfa82a59 100644
--- a/src/textual/dom.py
+++ b/src/textual/dom.py
@@ -56,12 +56,16 @@
     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 032c3ed3bd..021bcff0fa 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 4ebb0e17f3..9812836eed 100644
--- a/src/textual/geometry.py
+++ b/src/textual/geometry.py
@@ -28,6 +28,7 @@
 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)
 
@@ -1054,4 +1055,4 @@ def grow_maximum(self, other: Spacing) -> Spacing:
 
 
 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 9d96f3d6a5..0bc60d3623 100644
--- a/src/textual/message_pump.py
+++ b/src/textual/message_pump.py
@@ -340,7 +340,7 @@ def set_interval(
         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.
 
@@ -353,7 +353,7 @@ def call_after_refresh(self, callback: Callable, *args, **kwargs) -> None:
         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.
 
@@ -365,7 +365,7 @@ def call_later(self, callback: Callable, *args, **kwargs) -> None:
         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 b56213fbe8..121d80935d 100644
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -42,6 +42,9 @@
 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 1d590e2ed4..1a9f35b15c 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 @@
 
 
 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 eae882bc93..190bc00341 100644
--- a/src/textual/timer.py
+++ b/src/textual/timer.py
@@ -21,6 +21,7 @@
 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 0000000000..c9ad11a592
--- /dev/null
+++ b/src/textual/types.py
@@ -0,0 +1,20 @@
+"""
+Exports some types 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 9905041719..9e41011a32 100644
--- a/src/textual/worker.py
+++ b/src/textual/worker.py
@@ -100,6 +100,7 @@ class WorkerState(enum.Enum):
     Callable[[], ResultType],
     Awaitable[ResultType],
 ]
+"""Type used for [workers](/guide/workers/)."""
 
 
 class _ReprText:


Code review comments:
diff_hunk: @@ -399,33 +409,24 @@ def __init__(
 
     @property
     def workers(self) -> WorkerManager:
-        """The [worker](guide/workers/) manager.
-
-        Returns:
-            An object to manage workers.
-
-        """
+        """The [worker](guide/workers/) manager."""
         return self._workers
 
     @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].
-
         """
         return self._return_value
 
     @property
     def children(self) -> Sequence["Widget"]:
-        """A view on to the App's children.
+        """A view onto the app's direct children.
path: src/textual/app.py
text: @user1:
I think "immediate" would be more apt than "direct"