Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make mypy happy #1831

Merged
merged 71 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
7aa201e
Helper file for error progress tracking.
rodrigogiraoserrao Feb 17, 2023
e93b7e2
Fix typing of Keys.???.value.
rodrigogiraoserrao Feb 17, 2023
b709219
Assert that we have frame information.
rodrigogiraoserrao Feb 17, 2023
aad3660
Use inspect.Traceback instead of inspect.FrameInfo.
rodrigogiraoserrao Feb 17, 2023
148ce03
Update after installing msgpack-types.
rodrigogiraoserrao Feb 17, 2023
a410afa
Assert we have frame info.
rodrigogiraoserrao Feb 20, 2023
9f084b5
Fix MapGeometry order information typing.
rodrigogiraoserrao Feb 20, 2023
eeeac1a
Fix import and typing for fromkeys.
rodrigogiraoserrao Feb 20, 2023
cb091a3
Assert app is not None.
rodrigogiraoserrao Feb 20, 2023
2f9421d
Import missing type.
rodrigogiraoserrao Feb 20, 2023
ef8f46b
Use CallbackType for event Callback.
rodrigogiraoserrao Feb 20, 2023
4795910
Remove variable name clash.
rodrigogiraoserrao Feb 20, 2023
4f6e5c8
Ensure ScalarAnimation only receives widgets.
rodrigogiraoserrao Feb 20, 2023
d7f27c0
Count spacing values as 1 for int instance.
rodrigogiraoserrao Feb 20, 2023
4d60907
Use correct default.
rodrigogiraoserrao Feb 20, 2023
83356ed
Use correct abstract methods.
rodrigogiraoserrao Feb 20, 2023
e297c8c
Add missing annotation.
rodrigogiraoserrao Feb 20, 2023
0c52f70
Fix type inference.
rodrigogiraoserrao Feb 20, 2023
0ae4633
Check token is not None.
rodrigogiraoserrao Feb 20, 2023
c84a3c4
Revert "Check token is not None."
rodrigogiraoserrao Feb 21, 2023
4cea32d
Check that token is not None.
rodrigogiraoserrao Feb 21, 2023
b5fe26c
Type DOMQuery instantiation correctly.
rodrigogiraoserrao Feb 21, 2023
53d49f4
Infer correct type while walking.
rodrigogiraoserrao Feb 21, 2023
870e71b
Cast to remove doubt about None.
rodrigogiraoserrao Feb 21, 2023
54f9226
Explicitly type variable.
rodrigogiraoserrao Feb 21, 2023
8ae1665
Cast to console renderable.
rodrigogiraoserrao Feb 21, 2023
a1a059b
Type variable to remove literal ambiguity.
rodrigogiraoserrao Feb 21, 2023
83a94b2
Assert scrollbars always have parents.
rodrigogiraoserrao Feb 21, 2023
9d9c8cc
Make scrollbar scroll actions synchronous.
rodrigogiraoserrao Feb 21, 2023
5ca0763
Use link only when available.
rodrigogiraoserrao Feb 21, 2023
d3bad30
Merge branch 'main' into make-mypy-happy
rodrigogiraoserrao Feb 22, 2023
6fbf283
Update errors.
rodrigogiraoserrao Feb 22, 2023
79c2749
Relax type inference.
rodrigogiraoserrao Feb 22, 2023
232b70c
Ignore missing imports for uvloop.
rodrigogiraoserrao Feb 22, 2023
0f48929
Enable variable reuse.
rodrigogiraoserrao Feb 22, 2023
759b04a
Fix type issues in easing.py.
rodrigogiraoserrao Feb 23, 2023
f366783
Fix typing issues in demo.
rodrigogiraoserrao Feb 23, 2023
8b91a82
Fix typing issues in _doc.py.
rodrigogiraoserrao Feb 23, 2023
12a88fa
Type actions with a type alias.
rodrigogiraoserrao Feb 23, 2023
e7c0367
Make return values optional.
rodrigogiraoserrao Feb 23, 2023
208ddd1
Make StringKey idempotent.
rodrigogiraoserrao Feb 23, 2023
9277ed5
Make sorting type safe.
rodrigogiraoserrao Feb 23, 2023
21a0985
Fix typing of StringKey.__new__.
rodrigogiraoserrao Feb 23, 2023
8013641
Add explicit type variables.
rodrigogiraoserrao Feb 23, 2023
02b19c6
Type-safe line rendering.
rodrigogiraoserrao Feb 23, 2023
f22f257
Type safe _render_line_in_row.
rodrigogiraoserrao Feb 23, 2023
64db52e
Type safe _render_cell.
rodrigogiraoserrao Feb 23, 2023
6ee6189
Type safe ordered_rows property.
rodrigogiraoserrao Feb 23, 2023
7844c37
Type safe ordered_columns property.
rodrigogiraoserrao Feb 23, 2023
95e007b
Simplify handling of Nones in TwoWayDict.
rodrigogiraoserrao Feb 23, 2023
beb1906
Make use of idempotency of StringKey subclasses.
rodrigogiraoserrao Feb 23, 2023
99491d1
Make message aware of type of sender.
rodrigogiraoserrao Feb 23, 2023
b4418c5
Only select when possible.
rodrigogiraoserrao Feb 23, 2023
da8db5e
Fix typing of reactive attribute.
rodrigogiraoserrao Feb 23, 2023
4c605f4
Reset cursor upon moving action if needed.
rodrigogiraoserrao Feb 23, 2023
96403b2
Assure mypy we have ListItems.
rodrigogiraoserrao Feb 23, 2023
2f808c3
Add explicit return.
rodrigogiraoserrao Feb 23, 2023
bb7f58a
Ignore argument types to watch.
rodrigogiraoserrao Feb 23, 2023
84bc80e
Type safe App._register.
rodrigogiraoserrao Feb 23, 2023
26a8bb0
Redirect output to void.
rodrigogiraoserrao Feb 23, 2023
b99283e
Remove helper file.
rodrigogiraoserrao Feb 23, 2023
336f000
Fix Python compat. issues.
rodrigogiraoserrao Feb 24, 2023
a4072c4
Button can only accept str/Text as label.
rodrigogiraoserrao Feb 24, 2023
aadfd0d
Add runtime type check for list items.
rodrigogiraoserrao Feb 24, 2023
8675639
Address review comments.
rodrigogiraoserrao Feb 27, 2023
dccf167
Revert "Fix typing issues in demo."
rodrigogiraoserrao Feb 27, 2023
fa5d6cf
Address review comments.
rodrigogiraoserrao Feb 27, 2023
865ef14
Add clarifying comment.
rodrigogiraoserrao Feb 27, 2023
cb612c0
Merge branch 'main' into make-mypy-happy
rodrigogiraoserrao Feb 27, 2023
db3372d
Revert changes to data table.
rodrigogiraoserrao Mar 1, 2023
844d9c1
Merge branch 'main' into make-mypy-happy
rodrigogiraoserrao Mar 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Scrolling by page now adds to current position.
- Markdown lists have been polished: a selection of bullets, better alignment of numbers, style tweaks https://github.com/Textualize/textual/pull/1832
- Added alternative method of composing Widgets https://github.com/Textualize/textual/pull/1847
- Buttons no longer accept arbitrary renderables https://github.com/Textualize/textual/issues/1870

### Removed

Expand Down
4 changes: 4 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ ignore_missing_imports = True

[mypy-ipywidgets.*]
ignore_missing_imports = True

[mypy-uvloop.*]
# Ignore missing imports for optional library that isn't listed as a dependency.
ignore_missing_imports = True
5 changes: 4 additions & 1 deletion src/textual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ def __call__(self, *args: object, **kwargs) -> None:
if app.devtools is None or not app.devtools.is_connected:
return

previous_frame = inspect.currentframe().f_back
current_frame = inspect.currentframe()
assert current_frame is not None
previous_frame = current_frame.f_back
assert previous_frame is not None
caller = inspect.getframeinfo(previous_frame)

_log = self._log or app._log
Expand Down
1 change: 1 addition & 0 deletions src/textual/_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ async def invoke(callback: Callable, *params: object) -> Any:
# In debug mode we will warn about callbacks that may be stuck
def log_slow() -> None:
"""Log a message regarding a slow callback."""
assert app is not None
app.log.warning(
f"Callback {callback} is still pending after {INVOKE_TIMEOUT_WARNING} seconds"
)
Expand Down
53 changes: 33 additions & 20 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from __future__ import annotations

from operator import itemgetter
from typing import TYPE_CHECKING, Iterable, NamedTuple, cast
from typing import TYPE_CHECKING, Callable, Iterable, NamedTuple, cast

import rich.repr
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
Expand Down Expand Up @@ -45,12 +45,23 @@ class ReflowResult(NamedTuple):
class MapGeometry(NamedTuple):
"""Defines the absolute location of a Widget."""

region: Region # The (screen) region occupied by the widget
order: tuple[tuple[int, ...], ...] # A tuple of ints defining the painting order
clip: Region # A region to clip the widget by (if a Widget is within a container)
virtual_size: Size # The virtual size (scrollable region) of a widget if it is a container
container_size: Size # The container size (area not occupied by scrollbars)
virtual_region: Region # The region relative to the container (but not necessarily visible)
region: Region
"""The (screen) region occupied by the widget."""
order: tuple[tuple[int, int, int], ...]
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
"""Tuple of tuples defining the painting order of the widget.

Each successive triple represents painting order information with regards to
ancestors in the DOM hierarchy and the last triple provides painting order
information for this specific widget.
"""
clip: Region
"""A region to clip the widget by (if a Widget is within a container)."""
virtual_size: Size
"""The virtual size (scrollable region) of a widget if it is a container."""
container_size: Size
"""The container size (area not occupied by scrollbars)."""
virtual_region: Region
"""The region relative to the container (but not necessarily visible)."""

@property
def visible_region(self) -> Region:
Expand Down Expand Up @@ -419,19 +430,23 @@ def add_widget(
widget: Widget,
virtual_region: Region,
region: Region,
order: tuple[tuple[int, ...], ...],
order: tuple[tuple[int, int, int], ...],
layer_order: int,
clip: Region,
visible: bool,
_MapGeometry=MapGeometry,
_MapGeometry: type[MapGeometry] = MapGeometry,
) -> None:
"""Called recursively to place a widget and its children in the map.

Args:
widget: The widget to add.
virtual_region: TODO.
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
region: The region the widget will occupy.
order: A tuple of ints to define the order.
order: Painting order information.
layer_order: The order of the widget in its layer.
clip: The clipping region (i.e. the viewport which contains it).
visible: Whether the widget should be visible by default.
This may be overriden by the CSS rule `visibility`.
"""
visibility = widget.styles.get_rule("visibility")
if visibility is not None:
Expand Down Expand Up @@ -501,11 +516,12 @@ def add_widget(
)
widget_region = sub_region + placement_scroll_offset

widget_order = (
*order,
get_layer_index(sub_widget.layer, 0),
z,
layer_order,
widget_order = order + (
(
get_layer_index(sub_widget.layer, 0),
z,
layer_order,
),
)

add_widget(
Expand Down Expand Up @@ -560,7 +576,7 @@ def add_widget(
root,
size.region,
size.region,
((0,),),
((0, 0, 0),),
layer_order,
size.region,
True,
Expand Down Expand Up @@ -819,10 +835,7 @@ def render(self, full: bool = False) -> RenderableType | None:
cuts = self.cuts

# dict.fromkeys is a callable which takes a list of ints returns a dict which maps ints on to a list of Segments or None.
fromkeys = cast(
"Callable[[list[int]], dict[int, list[Segment] | None]]", dict.fromkeys
)
# A mapping of cut index to a list of segments for each line
fromkeys = cast("Callable[[list[int]], dict[int, Strip | None]]", dict.fromkeys)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment above needs updating.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff isn't great, but the comment is correct: it's "# dict.fromkeys is a callable [...]" and is the old line 821 / the new line 837...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment references list of Segments or None, but the annotation references Strip | None

chops: list[dict[int, Strip | None]]
chops = [fromkeys(cut_set[:-1]) for cut_set in cuts]

Expand Down
15 changes: 10 additions & 5 deletions src/textual/_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import shlex
from pathlib import Path
from typing import Iterable
from typing import Iterable, cast

from textual._import_app import import_app
from textual.app import App
Expand Down Expand Up @@ -45,6 +45,7 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
import traceback

traceback.print_exception(error)
return ""


def take_svg_screenshot(
Expand Down Expand Up @@ -82,6 +83,7 @@ def get_cache_key(app: App) -> str:
hash = hashlib.md5()
file_paths = [app_path] + app.css_path
for path in file_paths:
assert path is not None
with open(path, "rb") as source_file:
hash.update(source_file.read())
hash.update(f"{press}-{title}-{terminal_size}".encode("utf-8"))
Expand All @@ -105,10 +107,13 @@ async def auto_pilot(pilot: Pilot) -> None:

app.exit(svg)

svg = app.run(
headless=True,
auto_pilot=auto_pilot,
size=terminal_size,
svg = cast(
str,
app.run(
headless=True,
auto_pilot=auto_pilot,
size=terminal_size,
),
)

if app_path is not None:
Expand Down
4 changes: 2 additions & 2 deletions src/textual/_styles_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
from rich.style import Style

from ._border import get_box, render_row
from .filter import LineFilter
from ._opacity import _apply_opacity
from ._segment_tools import line_pad, line_trim
from .color import Color
from .filter import LineFilter
from .geometry import Region, Size, Spacing
from .renderables.text_opacity import TextOpacity
from .renderables.tint import Tint
from .strip import Strip

if TYPE_CHECKING:
from typing import TypeAlias
from typing_extensions import TypeAlias

from .css.styles import StylesBase
from .widget import Widget
Expand Down
4 changes: 2 additions & 2 deletions src/textual/_two_way_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __delitem__(self, key: Key) -> None:
self._forward.__delitem__(key)
self._reverse.__delitem__(value)

def get(self, key: Key) -> Value:
def get(self, key: Key) -> Value | None:
"""Given a key, efficiently lookup and return the associated value.

Args:
Expand All @@ -40,7 +40,7 @@ def get(self, key: Key) -> Value:
"""
return self._forward.get(key)

def get_key(self, value: Value) -> Key:
def get_key(self, value: Value) -> Key | None:
"""Given a value, efficiently lookup and return the associated key.

Args:
Expand Down
4 changes: 2 additions & 2 deletions src/textual/_xterm_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None:
if not bracketed_paste:
# Was it a pressed key event that we received?
key_events = list(sequence_to_key_events(sequence))
for event in key_events:
on_token(event)
for key_event in key_events:
on_token(key_event)
if key_events:
break
# Or a mouse event?
Expand Down
7 changes: 6 additions & 1 deletion src/textual/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import ast
import re

from typing_extensions import Any, TypeAlias

Action: TypeAlias = "tuple[str, tuple[Any, ...]]"
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
"""An action is its name and the arbitrary tuple of its parameters."""


class SkipAction(Exception):
"""Raise in an action to skip the action (and allow any parent bindings to run)."""
Expand All @@ -15,7 +20,7 @@ class ActionError(Exception):
re_action_params = re.compile(r"([\w\.]+)(\(.*?\))")


def parse(action: str) -> tuple[str, tuple[object, ...]]:
def parse(action: str) -> Action:
"""Parses an action string.

Args:
Expand Down
32 changes: 19 additions & 13 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
from ._event_broker import NoHandler, extract_handler_actions
from ._path import _make_path_object_relative
from ._wait import wait_for_idle
from .actions import SkipAction
from .actions import Action, SkipAction
from .await_remove import AwaitRemove
from .binding import Binding, Bindings
from .css.query import NoMatches
Expand Down Expand Up @@ -648,7 +648,7 @@ def _log(
self,
group: LogGroup,
verbosity: LogVerbosity,
_textual_calling_frame: inspect.FrameInfo,
_textual_calling_frame: inspect.Traceback,
*objects: Any,
**kwargs,
) -> None:
Expand Down Expand Up @@ -1608,9 +1608,8 @@ async def invoke_ready_callback() -> None:
with redirect_stdout(redirector): # type: ignore
await run_process_messages()
else:
null_file = _NullFile()
with redirect_stderr(null_file):
with redirect_stdout(null_file):
with redirect_stderr(None):
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
with redirect_stdout(None):
await run_process_messages()

finally:
Expand Down Expand Up @@ -1735,13 +1734,16 @@ def _register(
if not widgets:
return []

new_widgets = list(widgets)
widget_list = list(widgets)
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved

new_widgets: Iterable[Widget]
if before is not None or after is not None:
# There's a before or after, which means there's going to be an
# insertion, so make it easier to get the new things in the
# correct order.
new_widgets = reversed(new_widgets)
new_widgets = reversed(widget_list)
else:
new_widgets = widget_list

apply_stylesheet = self.stylesheet.apply
for widget in new_widgets:
Expand Down Expand Up @@ -1801,6 +1803,7 @@ def is_mounted(self, widget: Widget) -> bool:
async def _close_all(self) -> None:
"""Close all message pumps."""

screen: Screen | Callable[[], Screen] # Explicit type to reuse variable below.
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
# Close all screens on the stack
for screen in reversed(self._screen_stack):
if screen._running:
Expand Down Expand Up @@ -1974,7 +1977,7 @@ async def on_event(self, event: events.Event) -> None:

async def action(
self,
action: str | tuple[str, tuple[str, ...]],
action: str | Action,
default_namespace: object | None = None,
) -> bool:
"""Perform an action.
Expand Down Expand Up @@ -2072,7 +2075,7 @@ async def _broker_event(
else:
event.stop()
if isinstance(action, (str, tuple)):
await self.action(action, default_namespace=default_namespace)
await self.action(action, default_namespace=default_namespace) # type: ignore[arg-type]
elif callable(action):
await action()
else:
Expand Down Expand Up @@ -2339,20 +2342,23 @@ def _end_update(self) -> None:


def _init_uvloop() -> None:
"""
Import and install the `uvloop` asyncio policy, if available.
"""Import and install the `uvloop` asyncio policy, if available.

This is done only once, even if the function is called multiple times.

This is provided as a nicety for users that have `uvloop` installed independently
of Textual, as `uvloop` is not listed as a Textual dependency.
"""
global _uvloop_init_done

if _uvloop_init_done:
return

try:
import uvloop
import uvloop # type: ignore[reportMissingImports]
except ImportError:
pass
else:
uvloop.install()
uvloop.install() # type: ignore[reportUnknownMemberType]

_uvloop_init_done = True
3 changes: 2 additions & 1 deletion src/textual/cli/previews/easing.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def _animation_complete():
target_position = (
END_POSITION if self.position == START_POSITION else START_POSITION
)
assert event.button.id is not None # Should be set to an easing function str.
self.animate(
"position",
value=target_position,
Expand All @@ -106,7 +107,7 @@ def watch_position(self, value: int):
self.opacity_widget.styles.opacity = 1 - value / END_POSITION

def on_input_changed(self, event: Input.Changed):
if event.sender.id == "duration-input":
if event.input.id == "duration-input":
new_duration = _try_float(event.value)
if new_duration is not None:
self.duration = new_duration
Expand Down
8 changes: 5 additions & 3 deletions src/textual/css/_style_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,8 @@ def __set__(self, obj: StylesBase, spacing: SpacingDimensions | None):
string (e.g. ``"blue on #f0f0f0"``).

Raises:
ValueError: When the value is malformed, e.g. a ``tuple`` with a length that is
not 1, 2, or 4.
ValueError: When the value is malformed,
e.g. a ``tuple`` with a length that is not 1, 2, or 4.
"""
_rich_traceback_omit = True
if spacing is None:
Expand All @@ -543,7 +543,9 @@ def __set__(self, obj: StylesBase, spacing: SpacingDimensions | None):
str(error),
help_text=spacing_wrong_number_of_values_help_text(
property_name=self.name,
num_values_supplied=len(spacing),
num_values_supplied=(
1 if isinstance(spacing, int) else len(spacing)
),
context="inline",
),
)
Expand Down
Loading