From b902b1cae6aeb68383834988b9b3dfb7f97d5c99 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: Wed, 22 Nov 2023 15:50:20 +0000 Subject: [PATCH] Improve documentation. --- CHANGELOG.md | 3 +++ docs/api/await_complete.md | 1 + docs/widgets/markdown_viewer.md | 6 ++++++ docs/widgets/tree.md | 2 +- mkdocs-nav.yml | 1 + src/textual/_text_area_theme.py | 4 ++-- src/textual/await_remove.py | 1 - src/textual/document/_document.py | 6 +++--- src/textual/types.py | 16 +++++++++++++-- src/textual/widgets/_data_table.py | 1 + src/textual/widgets/_directory_tree.py | 2 +- src/textual/widgets/_loading_indicator.py | 2 +- src/textual/widgets/_markdown.py | 25 +++++++++++++++-------- src/textual/widgets/_option_list.py | 6 +++--- src/textual/widgets/_placeholder.py | 7 ++++--- src/textual/widgets/_tabbed_content.py | 2 +- src/textual/widgets/_tree.py | 22 +++++++++++--------- src/textual/widgets/markdown.py | 14 +++++++++++-- src/textual/widgets/tree.py | 18 ++++++++++++++-- tests/tree/test_tree_clearing.py | 4 ++-- tests/tree/test_tree_get_node_by_id.py | 4 ++-- 21 files changed, 102 insertions(+), 45 deletions(-) create mode 100644 docs/api/await_complete.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1da5c856..6a2b084747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Rich markup in markdown headings is now escaped when building the TOC https://github.com/Textualize/textual/issues/3689 - Mechanics behind mouse clicks. See [this](https://github.com/Textualize/textual/pull/3495#issue-1934915047) for more details. https://github.com/Textualize/textual/pull/3495 - Breaking change: max/min-width/height now includes padding and border. https://github.com/Textualize/textual/pull/3712 +- Method `MarkdownTableOfContents.set_table_of_contents` renamed to `MarkdownTableOfContents.rebuild_table_of_contents` https://github.com/Textualize/textual/pull/3730 +- Exception `Tree.UnknownNodeID` moved out of `Tree`, import from `textual.widgets.tree` https://github.com/Textualize/textual/pull/3730 +- Exception `TreeNode.RemoveRootError` moved out of `TreeNode`, import from `textual.widgets.tree` https://github.com/Textualize/textual/pull/3730 ## [0.41.0] - 2023-10-31 diff --git a/docs/api/await_complete.md b/docs/api/await_complete.md new file mode 100644 index 0000000000..523cb8a289 --- /dev/null +++ b/docs/api/await_complete.md @@ -0,0 +1 @@ +::: textual.await_complete diff --git a/docs/widgets/markdown_viewer.md b/docs/widgets/markdown_viewer.md index d830281fd4..cb209b2801 100644 --- a/docs/widgets/markdown_viewer.md +++ b/docs/widgets/markdown_viewer.md @@ -57,3 +57,9 @@ This widget has no component classes. ::: textual.widgets.MarkdownViewer options: heading_level: 2 + + +::: textual.widgets.markdown + options: + show_root_heading: true + show_root_toc_entry: true diff --git a/docs/widgets/tree.md b/docs/widgets/tree.md index 70d2822321..e1c4f33d1e 100644 --- a/docs/widgets/tree.md +++ b/docs/widgets/tree.md @@ -69,6 +69,6 @@ The tree widget provides the following component classes: --- -::: textual.widgets.tree.TreeNode +::: textual.widgets.tree options: heading_level: 2 diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index 2d61063111..9883f6b224 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -169,6 +169,7 @@ nav: - API: - "api/index.md" - "api/app.md" + - "api/await_complete.md" - "api/await_remove.md" - "api/binding.md" - "api/color.md" diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 93bad81c85..33845e4494 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -110,7 +110,7 @@ def __post_init__(self) -> None: ) @classmethod - def get_builtin_theme(cls, theme_name: str) -> "TextAreaTheme" | None: + def get_builtin_theme(cls, theme_name: str) -> TextAreaTheme | None: """Get a `TextAreaTheme` by name. Given a `theme_name`, return the corresponding `TextAreaTheme` object. @@ -120,7 +120,7 @@ def get_builtin_theme(cls, theme_name: str) -> "TextAreaTheme" | None: Returns: The `TextAreaTheme` corresponding to the name or `None` if the theme isn't - found. + found. """ return _BUILTIN_THEMES.get(theme_name) diff --git a/src/textual/await_remove.py b/src/textual/await_remove.py index 854703623a..f02fe5b840 100644 --- a/src/textual/await_remove.py +++ b/src/textual/await_remove.py @@ -1,5 +1,4 @@ """ - An *optionally* awaitable object returned by methods that remove widgets. """ diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py index 783a829e98..4fc8076f61 100644 --- a/src/textual/document/_document.py +++ b/src/textual/document/_document.py @@ -133,10 +133,10 @@ def get_size(self, indent_width: int) -> Size: def query_syntax_tree( self, - query: "Query", + query: Query, start_point: tuple[int, int] | None = None, end_point: tuple[int, int] | None = None, - ) -> list[tuple["Node", str]]: + ) -> list[tuple[Node, str]]: """Query the tree-sitter syntax tree. The default implementation always returns an empty list. @@ -153,7 +153,7 @@ def query_syntax_tree( """ return [] - def prepare_query(self, query: str) -> "Query" | None: + def prepare_query(self, query: str) -> Query | None: return None @property diff --git a/src/textual/types.py b/src/textual/types.py index 8c93663c2d..33be4449fe 100644 --- a/src/textual/types.py +++ b/src/textual/types.py @@ -14,8 +14,15 @@ ) from .actions import ActionParseResult from .css.styles import RenderStyles -from .widgets._data_table import CursorType +from .widgets._directory_tree import DirEntry from .widgets._input import InputValidationOn +from .widgets._option_list import ( + DuplicateID, + NewOptionListContent, + OptionDoesNotExist, + OptionListContent, +) +from .widgets._placeholder import PlaceholderVariant from .widgets._select import NoSelection, SelectType __all__ = [ @@ -24,13 +31,18 @@ "CallbackType", "CSSPathError", "CSSPathType", - "CursorType", + "DirEntry", + "DuplicateID", "EasingFunction", "IgnoreReturnCallbackType", "InputValidationOn", "MessageTarget", + "NewOptionListContent", "NoActiveAppError", "NoSelection", + "OptionDoesNotExist", + "OptionListContent", + "PlaceholderVariant", "RenderStyles", "SelectType", "UnusedParameter", diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 3f4aec4944..b01f212067 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -40,6 +40,7 @@ CursorType = Literal["cell", "row", "column", "none"] """The valid types of cursors for [`DataTable.cursor_type`][textual.widgets.DataTable.cursor_type].""" CellType = TypeVar("CellType") +"""Type used for cells in the DataTable.""" _DEFAULT_CELL_X_PADDING = 1 """Default padding to use on each side of a column in the data table.""" diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index d428939e53..688ca068ca 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -22,7 +22,7 @@ @dataclass class DirEntry: - """Attaches directory information to a node.""" + """Attaches directory information to a [DirectoryTree][textual.widgets.DirectoryTree] node.""" path: Path """The path of the directory entry.""" diff --git a/src/textual/widgets/_loading_indicator.py b/src/textual/widgets/_loading_indicator.py index 5931762850..a61a476e8d 100644 --- a/src/textual/widgets/_loading_indicator.py +++ b/src/textual/widgets/_loading_indicator.py @@ -71,7 +71,7 @@ def apply(self, widget: Widget) -> AwaitMount: widget: A widget. Returns: - AwaitMount: An awaitable for mounting the indicator. + An awaitable for mounting the indicator. """ self.add_class("-overlay") await_mount = widget.mount(self) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index de8e75059a..94db766681 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path, PurePath -from typing import Callable, Iterable +from typing import Callable, Iterable, Optional from markdown_it import MarkdownIt from markdown_it.token import Token @@ -22,6 +22,10 @@ from ..widgets import Static, Tree TableOfContentsType: TypeAlias = "list[tuple[int, str, str | None]]" +"""Information about the table of contents of a markdown document. + +The triples encode the level, the label, and the optional block id of each heading. +""" class Navigator: @@ -709,7 +713,7 @@ def unhandled_token(self, token: Token) -> MarkdownBlock | None: """Process an unhandled token. Args: - token: The token to handle. + token: The MarkdownIt token to handle. Returns: Either a widget to be added to the output, or `None`. @@ -872,6 +876,8 @@ def update(self, markdown: str) -> AwaitMount: class MarkdownTableOfContents(Widget, can_focus_children=True): + """Displays a table of contents for a markdown document.""" + DEFAULT_CSS = """ MarkdownTableOfContents { width: auto; @@ -884,7 +890,8 @@ class MarkdownTableOfContents(Widget, can_focus_children=True): } """ - table_of_contents = reactive["TableOfContentsType | None"](None, init=False) + table_of_contents = reactive[Optional[TableOfContentsType]](None, init=False) + """Underlying data to populate the table of contents widget.""" def __init__( self, @@ -903,7 +910,7 @@ def __init__( classes: The CSS classes for the widget. disabled: Whether the widget is disabled or not. """ - self.markdown = markdown + self.markdown: Markdown = markdown """The Markdown document associated with this table of contents.""" super().__init__(name=name, id=id, classes=classes, disabled=disabled) @@ -917,10 +924,10 @@ def compose(self) -> ComposeResult: def watch_table_of_contents(self, table_of_contents: TableOfContentsType) -> None: """Triggered when the table of contents changes.""" - self.set_table_of_contents(table_of_contents) + self.rebuild_table_of_contents(table_of_contents) - def set_table_of_contents(self, table_of_contents: TableOfContentsType) -> None: - """Set the table of contents. + def rebuild_table_of_contents(self, table_of_contents: TableOfContentsType) -> None: + """Rebuilds the tree representation of the table of contents data. Args: table_of_contents: Table of contents. @@ -1005,12 +1012,12 @@ def __init__( @property def document(self) -> Markdown: - """The Markdown document object.""" + """The [Markdown][textual.widgets.Markdown] document widget.""" return self.query_one(Markdown) @property def table_of_contents(self) -> MarkdownTableOfContents: - """The table of contents widget""" + """The [table of contents][textual.widgets.markdown.MarkdownTableOfContents] widget.""" return self.query_one(MarkdownTableOfContents) def _on_mount(self, _: Mount) -> None: diff --git a/src/textual/widgets/_option_list.py b/src/textual/widgets/_option_list.py index bbe564e65d..bed8c1e01e 100644 --- a/src/textual/widgets/_option_list.py +++ b/src/textual/widgets/_option_list.py @@ -26,11 +26,11 @@ class DuplicateID(Exception): - """Exception raised if a duplicate ID is used.""" + """Raised if a duplicate ID is used when adding options to an option list.""" class OptionDoesNotExist(Exception): - """Exception raised when a request has been made for an option that doesn't exist.""" + """Raised when a request has been made for an option that doesn't exist.""" class Option: @@ -126,7 +126,7 @@ def __contains__(self, line: object) -> bool: """The type of a new item of option list content to be added to an option list. This type represents all of the types that will be accepted when adding new -content to the option list. This is a superset of `OptionListContent`. +content to the option list. This is a superset of [`OptionListContent`][textual.types.OptionListContent]. """ diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 21367631ea..9f4590e37e 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -3,19 +3,20 @@ from __future__ import annotations from itertools import cycle -from typing import Iterator +from typing import TYPE_CHECKING, Iterator from weakref import WeakKeyDictionary from rich.console import RenderableType from typing_extensions import Literal, Self -from textual.app import App - from .. import events from ..css._error_tools import friendly_list from ..reactive import Reactive, reactive from ..widget import Widget +if TYPE_CHECKING: + from textual.app import App + PlaceholderVariant = Literal["default", "size", "text"] """The different variants of placeholder.""" diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index 0a0f5be8a7..605b2f9521 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -306,7 +306,7 @@ def add_pane( Note: Only one of `before` or `after` can be provided. If both are - provided a `Tabs.TabError` will be raised. + provided an exception is raised. """ if isinstance(before, TabPane): before = before.id diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index c413d79109..72882c9bff 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -42,6 +42,14 @@ TOGGLE_STYLE = Style.from_meta({"toggle": True}) +class RemoveRootError(Exception): + """Exception raised when trying to remove the root of a [`TreeNode`][textual.widgets.tree.TreeNode].""" + + +class UnknownNodeID(Exception): + """Exception raised when referring to an unknown [`TreeNode`][textual.widgets.tree.TreeNode] ID.""" + + @dataclass class _TreeLine(Generic[TreeDataType]): path: list[TreeNode[TreeDataType]] @@ -352,9 +360,6 @@ def add_leaf( node = self.add(label, data, expand=False, allow_expand=False) return node - class RemoveRootError(Exception): - """Exception raised when trying to remove a tree's root node.""" - def _remove_children(self) -> None: """Remove child nodes of this node. @@ -381,10 +386,10 @@ def remove(self) -> None: """Remove this node from the tree. Raises: - TreeNode.RemoveRootError: If there is an attempt to remove the root. + RemoveRootError: If there is an attempt to remove the root. """ if self.is_root: - raise self.RemoveRootError("Attempt to remove the root node of a Tree.") + raise RemoveRootError("Attempt to remove the root node of a Tree.") self._remove() self._tree._invalidate() @@ -758,9 +763,6 @@ def get_node_at_line(self, line_no: int) -> TreeNode[TreeDataType] | None: else: return line.node - class UnknownNodeID(Exception): - """Exception raised when referring to an unknown `TreeNode` ID.""" - def get_node_by_id(self, node_id: NodeID) -> TreeNode[TreeDataType]: """Get a tree node by its ID. @@ -771,12 +773,12 @@ def get_node_by_id(self, node_id: NodeID) -> TreeNode[TreeDataType]: The node associated with that ID. Raises: - Tree.UnknownID: Raised if the `TreeNode` ID is unknown. + UnknownNodeID: Raised if the `TreeNode` ID is unknown. """ try: return self._tree_nodes[node_id] except KeyError: - raise self.UnknownNodeID(f"Unknown NodeID ({node_id}) in tree") from None + raise UnknownNodeID(f"Unknown NodeID ({node_id}) in tree") from None def validate_cursor_line(self, value: int) -> int: """Prevent cursor line from going outside of range. diff --git a/src/textual/widgets/markdown.py b/src/textual/widgets/markdown.py index b9b1fec2fd..2e2fa97057 100644 --- a/src/textual/widgets/markdown.py +++ b/src/textual/widgets/markdown.py @@ -1,3 +1,13 @@ -from ._markdown import Markdown, MarkdownBlock, MarkdownTableOfContents +from ._markdown import ( + Markdown, + MarkdownBlock, + MarkdownTableOfContents, + TableOfContentsType, +) -__all__ = ["MarkdownTableOfContents", "Markdown", "MarkdownBlock"] +__all__ = [ + "MarkdownTableOfContents", + "Markdown", + "MarkdownBlock", + "TableOfContentsType", +] diff --git a/src/textual/widgets/tree.py b/src/textual/widgets/tree.py index 2e315bc23d..70296bcaa2 100644 --- a/src/textual/widgets/tree.py +++ b/src/textual/widgets/tree.py @@ -1,5 +1,19 @@ """Make non-widget Tree support classes available.""" -from ._tree import TreeNode +from ._tree import ( + EventTreeDataType, + NodeID, + RemoveRootError, + TreeDataType, + TreeNode, + UnknownNodeID, +) -__all__ = ["TreeNode"] +__all__ = [ + "EventTreeDataType", + "NodeID", + "RemoveRootError", + "TreeDataType", + "TreeNode", + "UnknownNodeID", +] diff --git a/tests/tree/test_tree_clearing.py b/tests/tree/test_tree_clearing.py index bd868ee6de..02a2ce711d 100644 --- a/tests/tree/test_tree_clearing.py +++ b/tests/tree/test_tree_clearing.py @@ -4,7 +4,7 @@ from textual.app import App, ComposeResult from textual.widgets import Tree -from textual.widgets.tree import TreeNode +from textual.widgets.tree import RemoveRootError class VerseBody: @@ -106,5 +106,5 @@ async def test_tree_remove_children_of_root(): async def test_attempt_to_remove_root(): """Attempting to remove the root should be an error.""" async with TreeClearApp().run_test() as pilot: - with pytest.raises(TreeNode.RemoveRootError): + with pytest.raises(RemoveRootError): pilot.app.query_one(VerseTree).root.remove() diff --git a/tests/tree/test_tree_get_node_by_id.py b/tests/tree/test_tree_get_node_by_id.py index 62f481aa98..60f73f1864 100644 --- a/tests/tree/test_tree_get_node_by_id.py +++ b/tests/tree/test_tree_get_node_by_id.py @@ -3,7 +3,7 @@ import pytest from textual.widgets import Tree -from textual.widgets._tree import NodeID +from textual.widgets.tree import NodeID, UnknownNodeID def test_get_tree_node_by_id() -> None: @@ -14,5 +14,5 @@ def test_get_tree_node_by_id() -> None: assert tree.get_node_by_id(tree.root.id).id == tree.root.id assert tree.get_node_by_id(child.id).id == child.id assert tree.get_node_by_id(grandchild.id).id == grandchild.id - with pytest.raises(Tree.UnknownNodeID): + with pytest.raises(UnknownNodeID): tree.get_node_by_id(cast(NodeID, grandchild.id + 1000))