diff --git a/src/textual/_node_list.py b/src/textual/_node_list.py index 7032b0cc7..5a464405a 100644 --- a/src/textual/_node_list.py +++ b/src/textual/_node_list.py @@ -1,6 +1,7 @@ from __future__ import annotations +import sys -from typing import TYPE_CHECKING, Iterator, Sequence, overload +from typing import TYPE_CHECKING, Any, Iterator, Sequence, overload import rich.repr @@ -13,7 +14,7 @@ class DuplicateIds(Exception): @rich.repr.auto(angular=True) -class NodeList(Sequence): +class NodeList(Sequence[Widget]): """ A container for widgets that forms one level of hierarchy. @@ -46,10 +47,10 @@ class NodeList(Sequence): def __len__(self) -> int: return len(self._nodes) - def __contains__(self, widget: Widget) -> bool: + def __contains__(self, widget: object) -> bool: return widget in self._nodes - def index(self, widget: Widget) -> int: + def index(self, widget: Any, start: int = 0, stop: int = sys.maxsize) -> int: """Return the index of the given widget. Args: @@ -61,7 +62,7 @@ class NodeList(Sequence): Raises: ValueError: If the widget is not in the node list. """ - return self._nodes.index(widget) + return self._nodes.index(widget, start, stop) def _get_by_id(self, widget_id: str) -> Widget | None: """Get the widget for the given widget_id, or None if there's no matches in this list""" diff --git a/src/textual/css/errors.py b/src/textual/css/errors.py index c0a5ca95d..a326db43b 100644 --- a/src/textual/css/errors.py +++ b/src/textual/css/errors.py @@ -4,8 +4,7 @@ from rich.console import ConsoleOptions, Console, RenderResult from rich.traceback import Traceback from ._help_renderables import HelpText -from .tokenize import Token -from .tokenizer import TokenError +from .tokenizer import Token, TokenError class DeclarationError(Exception): @@ -32,7 +31,7 @@ class StyleValueError(ValueError): error is raised. """ - def __init__(self, *args, help_text: HelpText | None = None): + def __init__(self, *args: object, help_text: HelpText | None = None): super().__init__(*args) self.help_text = help_text diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py index 51c8bf622..ca5094fc8 100644 --- a/src/textual/css/tokenizer.py +++ b/src/textual/css/tokenizer.py @@ -242,7 +242,7 @@ class Tokenizer: while True: if line_no >= len(self.lines): raise EOFError( - self.path, self.code, line_no, col_no, "Unexpected end of file" + self.path, self.code, (line_no, col_no), "Unexpected end of file" ) line = self.lines[line_no] match = expect.search(line, col_no) diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py index ab13ca8ee..3e32f628f 100644 --- a/src/textual/devtools/server.py +++ b/src/textual/devtools/server.py @@ -40,8 +40,8 @@ async def _on_startup(app: Application) -> None: def _run_devtools(verbose: bool, exclude: list[str] | None = None) -> None: app = _make_devtools_aiohttp_app(verbose=verbose, exclude=exclude) - def noop_print(_: str): - return None + def noop_print(_: str) -> None: + pass try: run_app( @@ -80,4 +80,4 @@ def _make_devtools_aiohttp_app( if __name__ == "__main__": - _run_devtools() + _run_devtools(True) diff --git a/src/textual/geometry.py b/src/textual/geometry.py index e29790e30..2dbb2c22a 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -7,6 +7,7 @@ Functions and classes to manage terminal geometry (anything involving coordinate from __future__ import annotations from functools import lru_cache +import math from operator import attrgetter, itemgetter from typing import Any, Collection, NamedTuple, Tuple, TypeVar, Union, cast @@ -129,7 +130,7 @@ class Offset(NamedTuple): """ x1, y1 = self x2, y2 = other - distance = ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) ** 0.5 + distance = math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) return distance @@ -218,6 +219,8 @@ class Size(NamedTuple): def __contains__(self, other: Any) -> bool: try: x, y = other + assert isinstance(x, int) + assert isinstance(y, int) except Exception: raise TypeError( "Dimensions.__contains__ requires an iterable of two integers" diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 5d0427905..d13d1bb8d 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -3,24 +3,24 @@ from __future__ import annotations import rich.repr import asyncio -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generic if TYPE_CHECKING: - from .app import App + from .app import App, ReturnType @rich.repr.auto(angular=True) -class Pilot: +class Pilot(Generic[ReturnType]): """Pilot object to drive an app.""" - def __init__(self, app: App) -> None: + def __init__(self, app: App[ReturnType]) -> None: self._app = app def __rich_repr__(self) -> rich.repr.Result: yield "app", self._app @property - def app(self) -> App: + def app(self) -> App[ReturnType]: """App: A reference to the application.""" return self._app @@ -47,7 +47,7 @@ class Pilot: """Wait for any animation to complete.""" await self._app.animator.wait_for_idle() - async def exit(self, result: object) -> None: + async def exit(self, result: ReturnType) -> None: """Exit the app with the given result. Args: diff --git a/src/textual/renderables/sparkline.py b/src/textual/renderables/sparkline.py index 5630c6bb6..eb55a6d56 100644 --- a/src/textual/renderables/sparkline.py +++ b/src/textual/renderables/sparkline.py @@ -1,7 +1,7 @@ from __future__ import annotations import statistics -from typing import Sequence, Iterable, Callable, TypeVar +from typing import Generic, Sequence, Iterable, Callable, TypeVar from rich.color import Color from rich.console import ConsoleOptions, Console, RenderResult @@ -12,8 +12,9 @@ from textual.renderables._blend_colors import blend_colors T = TypeVar("T", int, float) +SummaryFunction = Callable[[Sequence[T]], float] -class Sparkline: +class Sparkline(Generic[T]): """A sparkline representing a series of data. Args: @@ -33,16 +34,16 @@ class Sparkline: width: int | None, min_color: Color = Color.from_rgb(0, 255, 0), max_color: Color = Color.from_rgb(255, 0, 0), - summary_function: Callable[[list[T]], float] = max, + summary_function: SummaryFunction[T] = max, ) -> None: - self.data = data + self.data: Sequence[T] = data self.width = width self.min_color = Style.from_color(min_color) self.max_color = Style.from_color(max_color) - self.summary_function = summary_function + self.summary_function: SummaryFunction[T] = summary_function @classmethod - def _buckets(cls, data: Sequence[T], num_buckets: int) -> Iterable[list[T]]: + def _buckets(cls, data: Sequence[T], num_buckets: int) -> Iterable[Sequence[T]]: """Partition ``data`` into ``num_buckets`` buckets. For example, the data [1, 2, 3, 4] partitioned into 2 buckets is [[1, 2], [3, 4]]. @@ -73,13 +74,15 @@ class Sparkline: minimum, maximum = min(self.data), max(self.data) extent = maximum - minimum or 1 - buckets = list(self._buckets(self.data, num_buckets=self.width)) + buckets = tuple(self._buckets(self.data, num_buckets=width)) bucket_index = 0 bars_rendered = 0 - step = len(buckets) / width + step = len(buckets) // width summary_function = self.summary_function min_color, max_color = self.min_color.color, self.max_color.color + assert min_color is not None + assert max_color is not None while bars_rendered < width: partition = buckets[int(bucket_index)] partition_summary = summary_function(partition) @@ -94,10 +97,10 @@ class Sparkline: if __name__ == "__main__": console = Console() - def last(l): + def last(l: Sequence[T]) -> T: return l[-1] - funcs = min, max, last, statistics.median, statistics.mean + funcs: Sequence[SummaryFunction[int]] = (min, max, last, statistics.median, statistics.mean) nums = [10, 2, 30, 60, 45, 20, 7, 8, 9, 10, 50, 13, 10, 6, 5, 4, 3, 7, 20] console.print(f"data = {nums}\n") for f in funcs: diff --git a/src/textual/renderables/text_opacity.py b/src/textual/renderables/text_opacity.py index dffb5667b..638a3458b 100644 --- a/src/textual/renderables/text_opacity.py +++ b/src/textual/renderables/text_opacity.py @@ -1,5 +1,5 @@ import functools -from typing import Iterable +from typing import Iterable, Iterator from rich.cells import cell_len from rich.color import Color @@ -63,6 +63,7 @@ class TextOpacity: _from_color = Style.from_color if opacity == 0: for text, style, control in segments: + assert style is not None invisible_style = _from_color(bgcolor=style.bgcolor) yield _Segment(cell_len(text) * " ", invisible_style) else: @@ -106,7 +107,7 @@ if __name__ == "__main__": opacity_panel = TextOpacity(panel, opacity=0.5) console.print(opacity_panel) - def frange(start, end, step): + def frange(start: float, end: float, step: float) -> Iterator[float]: current = start while current < end: yield current @@ -120,5 +121,5 @@ if __name__ == "__main__": with Live(opacity_panel, refresh_per_second=60) as live: for value in itertools.cycle(frange(0, 1, 0.05)): - opacity_panel.value = value + opacity_panel.opacity = value sleep(0.05) diff --git a/src/textual/strip.py b/src/textual/strip.py index 7a83d5e40..5a4c1404e 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -109,8 +109,8 @@ class Strip: def __len__(self) -> int: return len(self._segments) - def __eq__(self, strip: Strip) -> bool: - return ( + def __eq__(self, strip: object) -> bool: + return isinstance(strip, Strip) and ( self._segments == strip._segments and self.cell_length == strip.cell_length ) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index e23385502..d0d7622f2 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -31,12 +31,12 @@ TOGGLE_STYLE = Style.from_meta({"toggle": True}) @dataclass -class _TreeLine: - path: list[TreeNode] +class _TreeLine(Generic[TreeDataType]): + path: list[TreeNode[TreeDataType]] last: bool @property - def node(self) -> TreeNode: + def node(self) -> TreeNode[TreeDataType]: """TreeNode: The node associated with this line.""" return self.path[-1] @@ -74,7 +74,7 @@ class TreeNode(Generic[TreeDataType]): self._label = label self.data = data self._expanded = expanded - self._children: list[TreeNode] = [] + self._children: list[TreeNode[TreeDataType]] = [] self._hover_ = False self._selected_ = False @@ -466,11 +466,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self._updates += 1 self.refresh() - def select_node(self, node: TreeNode | None) -> None: + def select_node(self, node: TreeNode[TreeDataType] | None) -> None: """Move the cursor to the given node, or reset cursor. Args: - node (TreeNode | None): A tree node, or None to reset cursor. + node (TreeNode[TreeDataType] | None): A tree node, or None to reset cursor. """ self.cursor_line = -1 if node is None else node._line @@ -570,11 +570,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): """ self.scroll_to_region(Region(0, line, self.size.width, 1)) - def scroll_to_node(self, node: TreeNode) -> None: + def scroll_to_node(self, node: TreeNode[TreeDataType]) -> None: """Scroll to the given node. Args: - node (TreeNode): Node to scroll in to view. + node (TreeNode[TreeDataType]): Node to scroll in to view. """ line = node._line if line != -1: @@ -628,7 +628,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): root = self.root - def add_node(path: list[TreeNode], node: TreeNode, last: bool) -> None: + def add_node(path: list[TreeNode[TreeDataType]], node: TreeNode[TreeDataType], last: bool) -> None: child_path = [*path, node] node._line = len(lines) add_line(TreeLine(child_path, last))