From 1b0c94e8a0d51056d572bc6a221329f9f37b4e0c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Oct 2022 11:15:15 +0100 Subject: [PATCH 1/6] Remove draw.py, fix imports --- src/textual/draw.py | 10 - tests/devtools/conftest.py | 4 +- tests/devtools/test_devtools.py | 2 +- tests/devtools/test_devtools_client.py | 2 +- tests/devtools/test_redirect_output.py | 3 +- tests/layouts/test_factory.py | 2 +- tests/test_border.py | 2 +- tests/test_easing.py | 1 - tests/test_integration_layout.py | 325 ------------------------- tests/test_integration_scrolling.py | 1 + tests/test_layout_resolve.py | 1 + tests/test_resolve.py | 4 +- tests/test_screens.py | 1 - tests/test_segment_tools.py | 1 - tests/test_styles_cache.py | 6 +- tests/test_widget.py | 1 - tests/utilities/test_app.py | 3 +- 17 files changed, 15 insertions(+), 354 deletions(-) delete mode 100644 src/textual/draw.py delete mode 100644 tests/test_integration_layout.py diff --git a/src/textual/draw.py b/src/textual/draw.py deleted file mode 100644 index 17d2edbf23..0000000000 --- a/src/textual/draw.py +++ /dev/null @@ -1,10 +0,0 @@ -from enum import Enum - - -class DrawStyle(Enum): - NONE = "none" - ASCII = "ascii" - SQUARE = "square" - HEAVY = "heavy" - ROUNDED = "rounded" - DOUBLE = "double" diff --git a/tests/devtools/conftest.py b/tests/devtools/conftest.py index b819512e28..5b24a06553 100644 --- a/tests/devtools/conftest.py +++ b/tests/devtools/conftest.py @@ -1,9 +1,7 @@ -from io import StringIO - import pytest -from textual.devtools.server import _make_devtools_aiohttp_app from textual.devtools.client import DevtoolsClient +from textual.devtools.server import _make_devtools_aiohttp_app from textual.devtools.service import DevtoolsService diff --git a/tests/devtools/test_devtools.py b/tests/devtools/test_devtools.py index b8c65bfd1d..944de3fcac 100644 --- a/tests/devtools/test_devtools.py +++ b/tests/devtools/test_devtools.py @@ -1,12 +1,12 @@ from datetime import datetime +import msgpack import pytest import time_machine from rich.align import Align from rich.console import Console from rich.segment import Segment -import msgpack from tests.utilities.render import wait_for_predicate from textual.devtools.renderables import DevConsoleLog, DevConsoleNotice diff --git a/tests/devtools/test_devtools_client.py b/tests/devtools/test_devtools_client.py index 6f4eae22cb..1d7e8e8f7a 100644 --- a/tests/devtools/test_devtools_client.py +++ b/tests/devtools/test_devtools_client.py @@ -3,11 +3,11 @@ from asyncio import Queue from datetime import datetime +import msgpack import time_machine from aiohttp.web_ws import WebSocketResponse from rich.console import ConsoleDimensions from rich.panel import Panel -import msgpack from tests.utilities.render import wait_for_predicate from textual.devtools.client import DevtoolsClient diff --git a/tests/devtools/test_redirect_output.py b/tests/devtools/test_redirect_output.py index fd46599402..f3e253701f 100644 --- a/tests/devtools/test_redirect_output.py +++ b/tests/devtools/test_redirect_output.py @@ -1,10 +1,9 @@ -import json from contextlib import redirect_stdout from datetime import datetime +import msgpack import time_machine -import msgpack from textual.devtools.redirect_output import StdoutRedirector TIMESTAMP = 1649166819 diff --git a/tests/layouts/test_factory.py b/tests/layouts/test_factory.py index 97feb1f5c4..bece5ab6a6 100644 --- a/tests/layouts/test_factory.py +++ b/tests/layouts/test_factory.py @@ -1,7 +1,7 @@ import pytest -from textual.layouts.vertical import VerticalLayout from textual.layouts.factory import get_layout, MissingLayout +from textual.layouts.vertical import VerticalLayout def test_get_layout_valid_layout(): diff --git a/tests/test_border.py b/tests/test_border.py index 4e2d563900..852e713ba4 100644 --- a/tests/test_border.py +++ b/tests/test_border.py @@ -1,7 +1,7 @@ from rich.segment import Segment from rich.style import Style -from textual._border import get_box, render_row +from textual._border import render_row def test_border_render_row(): diff --git a/tests/test_easing.py b/tests/test_easing.py index abdfe0a700..d73d24c7f4 100644 --- a/tests/test_easing.py +++ b/tests/test_easing.py @@ -8,7 +8,6 @@ from textual._easing import EASING - POINTS = [ 0.0, 0.05, diff --git a/tests/test_integration_layout.py b/tests/test_integration_layout.py deleted file mode 100644 index cbfaaced94..0000000000 --- a/tests/test_integration_layout.py +++ /dev/null @@ -1,325 +0,0 @@ -from __future__ import annotations -from typing import cast, List, Sequence - -import pytest -from rich.console import RenderableType -from rich.text import Text - -from tests.utilities.test_app import AppTest -from textual.app import ComposeResult -from textual.css.types import EdgeType -from textual.geometry import Size -from textual.widget import Widget -from textual.widgets import Placeholder - -pytestmark = pytest.mark.integration_test - -# Let's allow ourselves some abbreviated names for those tests, -# in order to make the test cases a bit easier to read :-) -SCREEN_W = 100 # width of our Screens -SCREEN_H = 8 # height of our Screens -SCREEN_SIZE = Size(SCREEN_W, SCREEN_H) -PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets - -# As per Widget's CSS property, by default Widgets have a horizontal scrollbar of size 1 -# and a vertical scrollbar of size 2: -SCROLL_H_SIZE = 1 -SCROLL_V_SIZE = 2 - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - ( - "placeholders_count", - "root_container_style", - "placeholders_style", - "expected_root_widget_virtual_size", - "expected_placeholders_size", - "expected_placeholders_offset_x", - ), - ( - *[ - [ - 1, - f"border: {invisible_border_edge};", # #root has no visible border - "", # no specific placeholder style - # #root's virtual size=screen size - (SCREEN_W, SCREEN_H), - # placeholders width=same than screen :: height=default height - (SCREEN_W, PLACEHOLDERS_DEFAULT_H), - # placeholders should be at offset 0 - 0, - ] - for invisible_border_edge in ("", "none", "hidden") - ], - [ - 1, - "border: solid white;", # #root has a visible border - "", # no specific placeholder style - # #root's virtual size is smaller because of its borders - (SCREEN_W - 2, SCREEN_H - 2), - # placeholders width=same than screen, minus 2 borders :: height=default height minus 2 borders - (SCREEN_W - 2, PLACEHOLDERS_DEFAULT_H), - # placeholders should be at offset 1 because of #root's border - 1, - ], - [ - 4, - "border: solid white;", # #root has a visible border - "", # no specific placeholder style - # #root's virtual height should be as high as its stacked content - (SCREEN_W - 2 - 1, PLACEHOLDERS_DEFAULT_H * 4), - # placeholders width=same than screen, minus 2 borders, minus scrollbar :: height=default height minus 2 borders - (SCREEN_W - 2 - SCROLL_V_SIZE, PLACEHOLDERS_DEFAULT_H), - # placeholders should be at offset 1 because of #root's border - 1, - ], - [ - 1, - "border: solid white;", # #root has a visible border - "align: center top;", # placeholders are centered horizontally - # #root's virtual size=screen size - (SCREEN_W, SCREEN_H), - # placeholders width=same than screen, minus 2 borders :: height=default height - (SCREEN_W - 2, PLACEHOLDERS_DEFAULT_H), - # placeholders should be at offset 1 because of #root's border - 1, - ], - [ - 4, - "border: solid white;", # #root has a visible border - "align: center top;", # placeholders are centered horizontally - # #root's virtual height should be as high as its stacked content - ( - SCREEN_W - 2 - SCROLL_V_SIZE, - PLACEHOLDERS_DEFAULT_H * 4, - ), - # placeholders width=same than screen, minus 2 borders, minus scrollbar :: height=default height - (SCREEN_W - 2 - SCROLL_V_SIZE, PLACEHOLDERS_DEFAULT_H), - # placeholders should be at offset 1 because of #root's border - 1, - ], - ), -) -async def test_composition_of_vertical_container_with_children( - placeholders_count: int, - root_container_style: str, - placeholders_style: str, - expected_placeholders_size: tuple[int, int], - expected_root_widget_virtual_size: tuple[int, int], - expected_placeholders_offset_x: int, -): - class VerticalContainer(Widget): - DEFAULT_CSS = ( - """ - VerticalContainer { - layout: vertical; - overflow: hidden auto; - ${root_container_style} - } - - VerticalContainer Placeholder { - height: ${placeholders_height}; - ${placeholders_style} - } - """.replace( - "${root_container_style}", root_container_style - ) - .replace("${placeholders_height}", str(PLACEHOLDERS_DEFAULT_H)) - .replace("${placeholders_style}", placeholders_style) - ) - - class MyTestApp(AppTest): - def compose(self) -> ComposeResult: - placeholders = [ - Placeholder(id=f"placeholder_{i}", name=f"Placeholder #{i}") - for i in range(placeholders_count) - ] - - yield VerticalContainer(*placeholders, id="root") - - app = MyTestApp(size=SCREEN_SIZE, test_name="compositor") - - expected_screen_size = SCREEN_SIZE - - async with app.in_running_state(): - # root widget checks: - root_widget = cast(Widget, app.get_child("root")) - assert root_widget.outer_size == expected_screen_size - root_widget_region = app.screen.find_widget(root_widget).region - assert root_widget_region == ( - 0, - 0, - expected_screen_size.width, - expected_screen_size.height, - ) - - app_placeholders = cast(List[Widget], app.query("Placeholder")) - assert len(app_placeholders) == placeholders_count - - # placeholder widgets checks: - for placeholder in app_placeholders: - assert placeholder.outer_size == expected_placeholders_size - assert placeholder.styles.offset.x.value == 0.0 - assert app.screen.get_offset(placeholder).x == expected_placeholders_offset_x - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "edge_type,expected_box_inner_size,expected_box_size,expected_top_left_edge_color,expects_visible_char_at_top_left_edge", - ( - # These first 3 types of border edge types are synonyms, and display no borders: - ["", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False], - ["none", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False], - ["hidden", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False], - # Let's transition to "blank": we still see no visible border, but the size is increased - # as the gutter space is reserved the same way it would be with a border: - ["blank", Size(SCREEN_W - 2, 1), Size(SCREEN_W, 3), "#ffffff", False], - # And now for the "normally visible" border edge types: - # --> we see a visible border, and the size is increased: - *[ - [edge_style, Size(SCREEN_W - 2, 1), Size(SCREEN_W, 3), "#ffffff", True] - for edge_style in [ - "round", - "solid", - "double", - "dashed", - "heavy", - "inner", - "outer", - "hkey", - "vkey", - ] - ], - ), -) -async def test_border_edge_types_impact_on_widget_size( - edge_type: EdgeType, - expected_box_inner_size: Size, - expected_box_size: Size, - expected_top_left_edge_color: str, - expects_visible_char_at_top_left_edge: bool, -): - class BorderTarget(Widget): - def render(self) -> RenderableType: - return Text("border target", style="black on yellow", justify="center") - - border_target = BorderTarget() - border_target.styles.height = "auto" - border_target.styles.border = (edge_type, "white") - - class MyTestApp(AppTest): - def compose(self) -> ComposeResult: - yield border_target - - app = MyTestApp(size=SCREEN_SIZE, test_name="border_edge_types") - - await app.boot_and_shutdown() - - box_inner_size = Size( - border_target.content_region.width, - border_target.content_region.height, - ) - assert box_inner_size == expected_box_inner_size - - assert border_target.outer_size == expected_box_size - - top_left_edge_style = app.screen.get_style_at(0, 0) - top_left_edge_color = top_left_edge_style.color.name - assert top_left_edge_color.upper() == expected_top_left_edge_color.upper() - - top_left_edge_char = app.get_char_at(0, 0) - top_left_edge_char_is_a_visible_one = top_left_edge_char != " " - assert top_left_edge_char_is_a_visible_one == expects_visible_char_at_top_left_edge - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "large_widget_size,container_style,expected_large_widget_visible_region_size", - ( - # In these tests we're going to insert a "large widget" - # into a container with size (20,20). - # ---------------- let's start! - # no overflow/scrollbar instructions: no scrollbars - [Size(30, 30), "color: red", Size(20, 20)], - # explicit hiding of the overflow: no scrollbars either - [Size(30, 30), "overflow: hidden", Size(20, 20)], - # scrollbar for both directions - [ - Size(30, 30), - "overflow: auto", - Size( - 20 - SCROLL_V_SIZE, - 20 - SCROLL_H_SIZE, - ), - ], - # horizontal scrollbar - [Size(30, 30), "overflow-x: auto", Size(20, 20 - SCROLL_H_SIZE)], - # vertical scrollbar - [Size(30, 30), "overflow-y: auto", Size(20 - SCROLL_V_SIZE, 20)], - # scrollbar for both directions, custom scrollbar size - [Size(30, 30), ("overflow: auto", "scrollbar-size: 3 5"), Size(20 - 5, 20 - 3)], - # scrollbar for both directions, custom vertical scrollbar size - [ - Size(30, 30), - ("overflow: auto", "scrollbar-size-vertical: 3"), - Size(20 - 3, 20 - SCROLL_H_SIZE), - ], - # scrollbar for both directions, custom horizontal scrollbar size - [ - Size(30, 30), - ("overflow: auto", "scrollbar-size-horizontal: 3"), - Size(20 - SCROLL_V_SIZE, 20 - 3), - ], - # scrollbar needed only horizontally, custom scrollbar size - [ - Size(30, 20), - ("overflow: auto", "scrollbar-size: 3 3"), - Size(20, 20 - 3), - ], - ), -) -async def test_scrollbar_size_impact_on_the_layout( - large_widget_size: Size, - container_style: str | Sequence[str], - expected_large_widget_visible_region_size: Size, -): - class LargeWidget(Widget): - def on_mount(self): - self.styles.width = large_widget_size[0] - self.styles.height = large_widget_size[1] - - container_style_rules = ( - [container_style] if isinstance(container_style, str) else container_style - ) - - class LargeWidgetContainer(Widget): - # TODO: Once textual#581 ("Default versus User CSS") is solved the following CSS should just use the - # "LargeWidgetContainer" selector, without having to use a more specific one to be able to override Widget's CSS: - DEFAULT_CSS = """ - #large-widget-container { - width: 20; - height: 20; - ${container_style}; - } - """.replace( - "${container_style}", - ";\n".join(container_style_rules), - ) - - large_widget = LargeWidget() - large_widget.expand = False - container = LargeWidgetContainer(large_widget, id="large-widget-container") - - class MyTestApp(AppTest): - def compose(self) -> ComposeResult: - yield container - - app = MyTestApp(size=Size(40, 40), test_name="scrollbar_size_impact_on_the_layout") - - await app.boot_and_shutdown() - - compositor = app.screen._compositor - widgets_map = compositor.map - large_widget_visible_region_size = widgets_map[large_widget].visible_region.size - assert large_widget_visible_region_size == expected_large_widget_visible_region_size diff --git a/tests/test_integration_scrolling.py b/tests/test_integration_scrolling.py index 8c4175c62f..0cd8629777 100644 --- a/tests/test_integration_scrolling.py +++ b/tests/test_integration_scrolling.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Sequence, cast import pytest diff --git a/tests/test_layout_resolve.py b/tests/test_layout_resolve.py index 77ba2643ef..e5ce35424a 100644 --- a/tests/test_layout_resolve.py +++ b/tests/test_layout_resolve.py @@ -3,6 +3,7 @@ from typing import NamedTuple import pytest + from textual._layout_resolve import layout_resolve diff --git a/tests/test_resolve.py b/tests/test_resolve.py index 4fac5ded24..50b4a44b02 100644 --- a/tests/test_resolve.py +++ b/tests/test_resolve.py @@ -1,8 +1,8 @@ import pytest -from textual.geometry import Size -from textual.css.scalar import Scalar from textual._resolve import resolve +from textual.css.scalar import Scalar +from textual.geometry import Size def test_resolve_empty(): diff --git a/tests/test_screens.py b/tests/test_screens.py index 9a9365aa2f..cea3179e32 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -5,7 +5,6 @@ from textual.app import App, ScreenStackError from textual.screen import Screen - skip_py310 = pytest.mark.skipif( sys.version_info.minor == 10 and sys.version_info.major == 3, reason="segfault on py3.10", diff --git a/tests/test_segment_tools.py b/tests/test_segment_tools.py index 770f33e239..630114ed9d 100644 --- a/tests/test_segment_tools.py +++ b/tests/test_segment_tools.py @@ -1,7 +1,6 @@ from rich.segment import Segment from rich.style import Style - from textual._segment_tools import line_crop, line_trim, line_pad diff --git a/tests/test_styles_cache.py b/tests/test_styles_cache.py index d30c1cebfb..32795dca02 100644 --- a/tests/test_styles_cache.py +++ b/tests/test_styles_cache.py @@ -3,11 +3,11 @@ from rich.segment import Segment from rich.style import Style -from textual.color import Color -from textual.geometry import Region, Size -from textual.css.styles import Styles from textual._styles_cache import StylesCache from textual._types import Lines +from textual.color import Color +from textual.css.styles import Styles +from textual.geometry import Region, Size def _extract_content(lines: Lines): diff --git a/tests/test_widget.py b/tests/test_widget.py index bd06d4c223..9c81c3fe41 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -1,5 +1,4 @@ import pytest -from rich.style import Style from textual.app import App from textual.css.errors import StyleValueError diff --git a/tests/utilities/test_app.py b/tests/utilities/test_app.py index 545a0210f4..25678f28d3 100644 --- a/tests/utilities/test_app.py +++ b/tests/utilities/test_app.py @@ -14,12 +14,13 @@ from textual import events, errors from textual._ansi_sequences import SYNC_START from textual._clock import _Clock -from textual.app import WINDOWS from textual._context import active_app from textual.app import App, ComposeResult +from textual.app import WINDOWS from textual.driver import Driver from textual.geometry import Size, Region + # N.B. These classes would better be named TestApp/TestConsole/TestDriver/etc, # but it makes pytest emit warning as it will try to collect them as classes containing test cases :-/ From b2e7f96c1c0b91a3cb9ca2474d623ede6c68476d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Oct 2022 11:20:55 +0100 Subject: [PATCH 2/6] Remove unused types --- src/textual/css/styles.py | 2 +- src/textual/css/types.py | 2 -- src/textual/layouts/vertical.py | 2 +- src/textual/widgets/__init__.py | 3 +-- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 035460d44f..76bc76371f 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -608,7 +608,7 @@ def extract_rules( default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS. Returns: - list[tuple[str, Specificity5, Any]]]: A list containing a tuple of , . + list[tuple[str, Specificity6, Any]]]: A list containing a tuple of , . """ is_important = self.important.__contains__ diff --git a/src/textual/css/types.py b/src/textual/css/types.py index 6fb0929a40..24958900ca 100644 --- a/src/textual/css/types.py +++ b/src/textual/css/types.py @@ -42,6 +42,4 @@ TextAlign = Literal["left", "start", "center", "right", "end", "justify"] Specificity3 = Tuple[int, int, int] -Specificity4 = Tuple[int, int, int, int] -Specificity5 = Tuple[int, int, int, int, int] Specificity6 = Tuple[int, int, int, int, int, int] diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index d7afced192..9460bf0db0 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -1,7 +1,7 @@ from __future__ import annotations from fractions import Fraction -from typing import cast, TYPE_CHECKING +from typing import TYPE_CHECKING from ..geometry import Region, Size from .._layout import ArrangeResult, Layout, WidgetPlacement diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 634b667855..8ad5dba984 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -7,7 +7,6 @@ if typing.TYPE_CHECKING: from ..widget import Widget - # ⚠️For any new built-in Widget we create, not only we have to add them to the following list, but also to the # `__init__.pyi` file in this same folder - otherwise text editors and type checkers won't be able to "see" them. __all__ = [ @@ -25,9 +24,9 @@ "Welcome", ] - _WIDGETS_LAZY_LOADING_CACHE: dict[str, type[Widget]] = {} + # Let's decrease startup time by lazy loading our Widgets: def __getattr__(widget_class: str) -> type[Widget]: try: From 6b10895eacf11f9d8f1003188c2e7decd3fa9c87 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Oct 2022 11:37:39 +0100 Subject: [PATCH 3/6] Removing some more unused code --- src/textual/_filter.py | 1 - src/textual/_types.py | 1 - src/textual/_xterm_parser.py | 4 ++-- src/textual/actions.py | 10 ---------- src/textual/app.py | 13 +------------ src/textual/css/parse.py | 2 +- src/textual/css/styles.py | 1 - src/textual/css/tokenizer.py | 1 - src/textual/drivers/linux_driver.py | 1 - 9 files changed, 4 insertions(+), 30 deletions(-) diff --git a/src/textual/_filter.py b/src/textual/_filter.py index 86ca9858e9..3857d68126 100644 --- a/src/textual/_filter.py +++ b/src/textual/_filter.py @@ -15,7 +15,6 @@ class LineFilter(ABC): @abstractmethod def filter(self, segments: list[Segment]) -> list[Segment]: """Transform a list of segments.""" - ... class Monochrome(LineFilter): diff --git a/src/textual/_types.py b/src/textual/_types.py index 1dd59297cc..60d9439635 100644 --- a/src/textual/_types.py +++ b/src/textual/_types.py @@ -32,6 +32,5 @@ def post_message_no_wait(self, message: "Message") -> bool: ... -MessageHandler = Callable[["Message"], Awaitable] Lines = List[List[Segment]] CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]] diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index c1df3840c4..40af686a48 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -9,12 +9,12 @@ from ._ansi_sequences import ANSI_SEQUENCES_KEYS from ._parser import Awaitable, Parser, TokenCallback from ._types import MessageTarget +from .keys import KEY_NAME_REPLACEMENTS + # When trying to determine whether the current sequence is a supported/valid # escape sequence, at which length should we give up and consider our search # to be unsuccessful? -from .keys import KEY_NAME_REPLACEMENTS - _MAX_SEQUENCE_SEARCH_THRESHOLD = 20 _re_mouse_event = re.compile("^" + re.escape("\x1b[") + r"( tuple[str, tuple[object, ...]]: action_name, action_params if isinstance(action_params, tuple) else (action_params,), ) - - -if __name__ == "__main__": - - print(parse("foo")) - - print(parse("view.toggle('side')")) - - print(parse("view.toggle")) diff --git a/src/textual/app.py b/src/textual/app.py index 643ffe2d39..d286dcffe5 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -11,17 +11,12 @@ from datetime import datetime from pathlib import Path, PurePath from time import perf_counter -from typing import Any, Generic, Iterable, Iterator, Type, TypeVar, cast, Union +from typing import Any, Generic, Iterable, Type, TypeVar, cast, Union from weakref import WeakSet, WeakValueDictionary from ._ansi_sequences import SYNC_END, SYNC_START from ._path import _make_path_object_relative -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal # pragma: no cover - import nanoid import rich import rich.repr @@ -131,7 +126,6 @@ class App(Generic[ReturnType], DOMNode): title (str | None, optional): Title of the application. If ``None``, the title is set to the name of the ``App`` subclass. Defaults to ``None``. css_path (str | PurePath | None, optional): Path to CSS or ``None`` for no CSS file. Defaults to None. watch_css (bool, optional): Watch CSS for changes. Defaults to False. - """ # Inline CSS for quick scripts (generally css_path should be preferred.) @@ -702,11 +696,6 @@ def update_styles(self, node: DOMNode | None = None) -> None: self._require_stylesheet_update.add(self.screen if node is None else node) self.check_idle() - def update_visible_styles(self) -> None: - """Update visible styles only.""" - self._require_stylesheet_update.update(self.screen.visible_widgets) - self.check_idle() - def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: """Mount widgets. Widgets specified as positional args, or keywords args. If supplied as keyword args they will be assigned an id of the key. diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py index c9ba19fd2f..b26f171db1 100644 --- a/src/textual/css/parse.py +++ b/src/textual/css/parse.py @@ -2,7 +2,7 @@ from functools import lru_cache from pathlib import PurePath -from typing import Iterator, Iterable, NoReturn, Sequence +from typing import Iterator, Iterable, NoReturn from rich import print diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 76bc76371f..d59bea2f66 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -67,7 +67,6 @@ from typing_extensions import TypedDict if TYPE_CHECKING: - from .._animator import Animation from .._layout import Layout from ..dom import DOMNode diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py index 15dc90508d..51c8bf622f 100644 --- a/src/textual/css/tokenizer.py +++ b/src/textual/css/tokenizer.py @@ -13,7 +13,6 @@ from rich.text import Text from ._error_tools import friendly_list -from .._loop import loop_last class TokenError(Exception): diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 23fd24c127..eb8cd95ea3 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -17,7 +17,6 @@ import rich.repr from .. import log -from .. import events from ..driver import Driver from ..geometry import Size from .._types import MessageTarget From 14ea50d4c1bfe2ebf5dc2e1d549a7c77818c6329 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Oct 2022 12:19:07 +0100 Subject: [PATCH 4/6] Remove some unused exceptions --- src/textual/color.py | 1 - src/textual/dom.py | 4 ---- src/textual/message_pump.py | 4 ---- 3 files changed, 9 deletions(-) diff --git a/src/textual/color.py b/src/textual/color.py index d2645b8b49..16108e71d5 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -528,7 +528,6 @@ def get_contrast_text(self, alpha=0.95) -> Color: # Color constants WHITE = Color(255, 255, 255) BLACK = Color(0, 0, 0) -TRANSPARENT = Color(0, 0, 0, 0) def rgb_to_lab(rgb: Color) -> Lab: diff --git a/src/textual/dom.py b/src/textual/dom.py index 798b04d45c..f3234ac866 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -75,10 +75,6 @@ class NoScreen(DOMError): pass -class NoParent(Exception): - pass - - @rich.repr.auto class DOMNode(MessagePump): """The base class for object that can be in the Textual DOM (App and Widget)""" diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4124f66ecf..23bd8a8733 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -29,10 +29,6 @@ from .app import App -class NoParent(Exception): - pass - - class CallbackError(Exception): pass From 7f24a6dfa5f8466919fe24e22e4ecbbf97bc9f63 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Oct 2022 12:44:20 +0100 Subject: [PATCH 5/6] Remove E2E tests (snapshot covers same functionality), move custom theme into docs folder --- {custom_theme => docs/custom_theme}/main.html | 0 e2e_tests/sandbox_basic_test.py | 55 ---- e2e_tests/test_apps/basic.css | 250 ------------------ e2e_tests/test_apps/basic.py | 236 ----------------- mkdocs.yml | 2 +- src/textual/drivers/linux_driver.py | 2 +- src/textual/scrollbar.py | 2 +- 7 files changed, 3 insertions(+), 544 deletions(-) rename {custom_theme => docs/custom_theme}/main.html (100%) delete mode 100644 e2e_tests/sandbox_basic_test.py delete mode 100644 e2e_tests/test_apps/basic.css delete mode 100644 e2e_tests/test_apps/basic.py diff --git a/custom_theme/main.html b/docs/custom_theme/main.html similarity index 100% rename from custom_theme/main.html rename to docs/custom_theme/main.html diff --git a/e2e_tests/sandbox_basic_test.py b/e2e_tests/sandbox_basic_test.py deleted file mode 100644 index 6cd87ddac1..0000000000 --- a/e2e_tests/sandbox_basic_test.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -import shlex -import sys -import subprocess -import threading -from pathlib import Path - -target_script_name = "basic" -script_time_to_live = 2.0 # in seconds - -if len(sys.argv) > 1: - target_script_name = sys.argv[1] -if len(sys.argv) > 2: - script_time_to_live = float(sys.argv[2]) - -e2e_root = Path(__file__).parent / "test_apps" - -completed_process = None - - -def launch_sandbox_script(python_file_name: str) -> None: - global completed_process - - command = f"{sys.executable} {shlex.quote(python_file_name)}.py" - print(f"Launching command '{command}'...") - try: - completed_process = subprocess.run( - command, shell=True, check=True, capture_output=True, cwd=str(e2e_root) - ) - except subprocess.CalledProcessError as err: - print(f"Process error: {err.stderr}") - raise - - -thread = threading.Thread( - target=launch_sandbox_script, args=(target_script_name,), daemon=False -) -thread.start() - -print( - f"Launching Python script in a sub-thread; we'll wait for it for {script_time_to_live} seconds..." -) -thread.join(timeout=script_time_to_live) -print("The wait is over.") - -process_still_running = completed_process is None -process_was_able_to_run_without_errors = process_still_running - -if process_was_able_to_run_without_errors: - print("Python script is still running :-)") -else: - print("Python script is no longer running :-/") - -sys.exit(0 if process_was_able_to_run_without_errors else 1) diff --git a/e2e_tests/test_apps/basic.css b/e2e_tests/test_apps/basic.css deleted file mode 100644 index 88de0d5353..0000000000 --- a/e2e_tests/test_apps/basic.css +++ /dev/null @@ -1,250 +0,0 @@ -/* CSS file for basic.py */ - - - - * { - transition: color 300ms linear, background 300ms linear; -} - - -*:hover { - /* tint: 30% red; - /* outline: heavy red; */ -} - -App > Screen { - - background: $background; - color: $text; - layers: base sidebar; - layout: vertical; - overflow: hidden; -} - -#tree-container { - overflow-y: auto; - height: 20; - margin: 1 2; - background: $surface; - padding: 1 2; -} - -DirectoryTree { - padding: 0 1; - height: auto; - -} - - - - -DataTable { - /*border:heavy red;*/ - /* tint: 10% green; */ - /* text-opacity: 50%; */ - padding: 1; - margin: 1 2; - height: 24; -} - -#sidebar { - background: $panel; - color: $text; - dock: left; - width: 30; - margin-bottom: 1; - offset-x: -100%; - - transition: offset 500ms in_out_cubic; - layer: sidebar; -} - -#sidebar.-active { - offset-x: 0; -} - -#sidebar .title { - height: 1; - background: $primary-background-darken-1; - color: $text; - border-right: wide $background; - content-align: center middle; -} - -#sidebar .user { - height: 8; - background: $panel-darken-1; - color: $text; - border-right: wide $background; - content-align: center middle; -} - -#sidebar .content { - background: $panel-darken-2; - color: $text; - border-right: wide $background; - content-align: center middle; -} - - - - -Tweet { - height:12; - width: 100%; - margin: 0 2; - - margin:0 2; - background: $panel; - color: $text; - layout: vertical; - /* border: outer $primary; */ - padding: 1; - border: wide $panel; - overflow: auto; - /* scrollbar-gutter: stable; */ - align-horizontal: center; - box-sizing: border-box; - -} - - -.scrollable { - overflow-x: auto; - overflow-y: scroll; - margin: 1 2; - height: 24; - align-horizontal: center; - layout: vertical; -} - -.code { - height: auto; - -} - - -TweetHeader { - height:1; - background: $accent; - color: $text; -} - -TweetBody { - width: 100%; - background: $panel; - color: $text; - height: auto; - padding: 0 1 0 0; -} - -Tweet.scroll-horizontal TweetBody { - width: 350; -} - -.button { - background: $accent; - color: $text; - width:20; - height: 3; - /* border-top: hidden $accent-darken-3; */ - border: tall $accent-darken-2; - /* border-left: tall $accent-darken-1; */ - - - /* padding: 1 0 0 0 ; */ - - transition: background 400ms in_out_cubic, color 400ms in_out_cubic; - -} - -.button:hover { - background: $accent-lighten-1; - color: $text; - width: 20; - height: 3; - border: tall $accent-darken-1; - /* border-left: tall $accent-darken-3; */ - - - - -} - -#footer { - color: $text; - background: $accent; - height: 1; - - content-align: center middle; - dock:bottom; -} - - -#sidebar .content { - layout: vertical -} - -OptionItem { - height: 3; - background: $panel; - border-right: wide $background; - border-left: blank; - content-align: center middle; -} - -OptionItem:hover { - height: 3; - color: $text; - background: $primary-darken-1; - /* border-top: hkey $accent2-darken-3; - border-bottom: hkey $accent2-darken-3; */ - text-style: bold; - border-left: outer $secondary-darken-2; -} - -Error { - width: 100%; - height:3; - background: $error; - color: $text; - border-top: tall $error-darken-2; - border-bottom: tall $error-darken-2; - - padding: 0; - text-style: bold; - align-horizontal: center; -} - -Warning { - width: 100%; - height:3; - background: $warning; - color: $text; - border-top: tall $warning-darken-2; - border-bottom: tall $warning-darken-2; - - text-style: bold; - align-horizontal: center; -} - -Success { - width: 100%; - - height:auto; - box-sizing: border-box; - background: $success; - color: $text; - - border-top: hkey $success-darken-2; - border-bottom: hkey $success-darken-2; - - text-style: bold ; - - align-horizontal: center; -} - - -.horizontal { - layout: horizontal -} diff --git a/e2e_tests/test_apps/basic.py b/e2e_tests/test_apps/basic.py deleted file mode 100644 index 4b44a4f6eb..0000000000 --- a/e2e_tests/test_apps/basic.py +++ /dev/null @@ -1,236 +0,0 @@ -from rich.console import RenderableType - -from rich.syntax import Syntax -from rich.text import Text - -from textual.app import App, ComposeResult -from textual.reactive import Reactive -from textual.widget import Widget -from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer -from textual.containers import Container - -CODE = ''' -from __future__ import annotations - -from typing import Iterable, TypeVar - -T = TypeVar("T") - - -def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]: - """Iterate and generate a tuple with a flag for first value.""" - iter_values = iter(values) - try: - value = next(iter_values) - except StopIteration: - return - yield True, value - for value in iter_values: - yield False, value - - -def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]: - """Iterate and generate a tuple with a flag for last value.""" - iter_values = iter(values) - try: - previous_value = next(iter_values) - except StopIteration: - return - for value in iter_values: - yield False, previous_value - previous_value = value - yield True, previous_value - - -def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]: - """Iterate and generate a tuple with a flag for first and last value.""" - iter_values = iter(values) - try: - previous_value = next(iter_values) - except StopIteration: - return - first = True - for value in iter_values: - yield first, False, previous_value - first = False - previous_value = value - yield first, True, previous_value -''' - - -lorem_short = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit liber a a a, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum.""" -lorem = ( - lorem_short - + """ In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """ -) - -lorem_short_text = Text.from_markup(lorem_short) -lorem_long_text = Text.from_markup(lorem * 2) - - -class TweetHeader(Widget): - def render(self) -> RenderableType: - return Text("Lorem Impsum", justify="center") - - -class TweetBody(Widget): - short_lorem = Reactive(False) - - def render(self) -> Text: - return lorem_short_text if self.short_lorem else lorem_long_text - - -class Tweet(Widget): - pass - - -class OptionItem(Widget): - def render(self) -> Text: - return Text("Option") - - -class Error(Widget): - def render(self) -> Text: - return Text("This is an error message", justify="center") - - -class Warning(Widget): - def render(self) -> Text: - return Text("This is a warning message", justify="center") - - -class Success(Widget): - def render(self) -> Text: - return Text("This is a success message", justify="center") - - -class BasicApp(App): - """A basic app demonstrating CSS""" - - CSS_PATH = "basic.css" - - def on_load(self): - """Bind keys here.""" - self.bind("s", "toggle_class('#sidebar', '-active')", description="Sidebar") - self.bind("d", "toggle_dark", description="Dark mode") - self.bind("q", "quit", description="Quit") - self.bind("f", "query_test", description="Query test") - - def compose(self): - yield Header() - - table = DataTable() - self.scroll_to_target = Tweet(TweetBody()) - - yield Container( - Tweet(TweetBody()), - Widget( - Static( - Syntax(CODE, "python", line_numbers=True, indent_guides=True), - classes="code", - ), - classes="scrollable", - ), - table, - Widget(DirectoryTree("~/"), id="tree-container"), - Error(), - Tweet(TweetBody(), classes="scrollbar-size-custom"), - Warning(), - Tweet(TweetBody(), classes="scroll-horizontal"), - Success(), - Tweet(TweetBody(), classes="scroll-horizontal"), - Tweet(TweetBody(), classes="scroll-horizontal"), - Tweet(TweetBody(), classes="scroll-horizontal"), - Tweet(TweetBody(), classes="scroll-horizontal"), - Tweet(TweetBody(), classes="scroll-horizontal"), - ) - yield Widget( - Widget(classes="title"), - Widget(classes="user"), - OptionItem(), - OptionItem(), - OptionItem(), - Widget(classes="content"), - id="sidebar", - ) - yield Footer() - - table.add_column("Foo", width=20) - table.add_column("Bar", width=20) - table.add_column("Baz", width=20) - table.add_column("Foo", width=20) - table.add_column("Bar", width=20) - table.add_column("Baz", width=20) - table.zebra_stripes = True - for n in range(100): - table.add_row(*[f"Cell ([b]{n}[/b], {col})" for col in range(6)]) - - def on_mount(self): - self.sub_title = "Widget demo" - - async def on_key(self, event) -> None: - await self.dispatch_key(event) - - def action_toggle_dark(self): - self.dark = not self.dark - - def action_query_test(self): - query = self.query("Tweet") - self.log(query) - self.log(query.nodes) - self.log(query) - self.log(query.nodes) - - query.set_styles("outline: outer red;") - - query = query.exclude(".scroll-horizontal") - self.log(query) - self.log(query.nodes) - - # query = query.filter(".rubbish") - # self.log(query) - # self.log(query.first()) - - async def key_q(self): - await self.shutdown() - - def key_x(self): - self.panic(self.tree) - - def key_escape(self): - self.app.bell() - - def key_t(self): - # Pressing "t" toggles the content of the TweetBody widget, from a long "Lorem ipsum..." to a shorter one. - tweet_body = self.query("TweetBody").first() - tweet_body.short_lorem = not tweet_body.short_lorem - - def key_v(self): - self.get_child(id="content").scroll_to_widget(self.scroll_to_target) - - def key_space(self): - self.bell() - - -if __name__ == "__main__": - app = BasicApp() - app.run(quit_after=2) - - # from textual.geometry import Region - # from textual.color import Color - - # print(Region.intersection.cache_info()) - # print(Region.overlaps.cache_info()) - # print(Region.union.cache_info()) - # print(Region.split_vertical.cache_info()) - # print(Region.__contains__.cache_info()) - # from textual.css.scalar import Scalar - - # print(Scalar.resolve_dimension.cache_info()) - - # from rich.style import Style - # from rich.cells import cached_cell_len - - # print(Style._add.cache_info()) - - # print(cached_cell_len.cache_info()) diff --git a/mkdocs.yml b/mkdocs.yml index 5d9fca9b66..2f3de4218c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -155,7 +155,7 @@ markdown_extensions: theme: name: material - custom_dir: custom_theme + custom_dir: docs/custom_theme features: - navigation.tabs - navigation.indexes diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index eb8cd95ea3..e8c7bd00a7 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -22,6 +22,7 @@ from .._types import MessageTarget from .._xterm_parser import XTermParser from .._profile import timer +from .. import events @rich.repr.auto @@ -234,7 +235,6 @@ def more_data() -> bool: if __name__ == "__main__": from rich.console import Console - from .. import events console = Console() diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index 97eef75682..707cd67d2c 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -6,7 +6,7 @@ from rich.color import Color from rich.console import ConsoleOptions, RenderableType, RenderResult from rich.segment import Segment, Segments -from rich.style import NULL_STYLE, Style, StyleType +from rich.style import Style, StyleType from . import events from ._types import MessageTarget From e090931d213fa478eec2d8b5e3b91483e1f26a38 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 13 Oct 2022 12:55:50 +0100 Subject: [PATCH 6/6] Remove E2E smoke test from workflow --- .github/workflows/pythonpackage.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index b9edee5e03..ee439baf00 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -39,10 +39,6 @@ jobs: run: | source $VENV pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing - - name: Quick e2e smoke test - run: | - source $VENV - python e2e_tests/sandbox_basic_test.py basic 2.0 - name: Upload snapshot report if: always() uses: actions/upload-artifact@v3