diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ebd7ad..1cf6e8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Textual-Window Changelog +## [0.8.2] 2026-07-19 + +### Changed + +- Relaxed the Textual version constraint to `>=5.1.0,<9.0.0` to allow the latest Textual (verified against 8.2.8). +- Added a `TypedAppMixin` typing shim in `button_bases.py`. Textual 8.x declares `DOMNode.app` with `getters.app(App)`, which resolves to `App[Unknown]` and fails strict type checking; the mixin redeclares it as `App[object]` with no runtime effect. +- [dev] Added Textual 8.2 to the Nox test matrix alongside 5.1. +- [dev] Regenerated the snapshot baseline under Textual 8.2.8 and Python 3.9 (the version CI and `.python-version` use — Rich's SVG output is sensitive to both the Rich and Python versions). The snapshot test is skipped on Textual < 8, since older Rich versions emit a different SVG format. +- [dev] Updated `uv.lock` so the default dev environment uses the latest Textual. + ## [0.8.1] 2025-08-01 - Updated `ezpubsub` dependency to version 0.3.0 diff --git a/noxfile.py b/noxfile.py index 75555c3..164481f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -24,7 +24,7 @@ # PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12"] PYTHON_VERSIONS = ["3.9"] -MAJOR_TEXTUAL_VERSIONS = [5.1] +MAJOR_TEXTUAL_VERSIONS = [5.1, 8.2] ############## # NOX CONFIG # diff --git a/pyproject.toml b/pyproject.toml index 4128413..3af9948 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "textual-window" -version = "0.8.1" +version = "0.8.2" description = "A Textual widget for a floating, draggable window and included window bar/manager system." readme = "README.md" requires-python = ">=3.9" @@ -24,7 +24,7 @@ classifiers = [ dependencies = [ "ezpubsub>=0.3.0", - "textual>=5.1.0,<6.0.0", + "textual>=5.1.0,<9.0.0", ] [project.urls] diff --git a/src/textual_window/button_bases.py b/src/textual_window/button_bases.py index 274fef7..1189ebf 100644 --- a/src/textual_window/button_bases.py +++ b/src/textual_window/button_bases.py @@ -2,13 +2,34 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from textual.visual import VisualType from textual import events from textual.widgets import Static from textual.message import Message +if TYPE_CHECKING: + from textual.app import App + + +class TypedAppMixin: + """Redeclares `self.app` as `App[object]` for the type checkers. + + Textual 8.x types `DOMNode.app` with `getters.app(App)`, which resolves to + `App[Unknown]` and fails strict Pyright/MyPy wherever `self.app` is used. + This mixin has no runtime effect.""" + + if TYPE_CHECKING: + + @property + def app(self) -> App[object]: ... + + @app.setter + def app(self, app: App[object]) -> None: ... + -class NoSelectStatic(Static): +class NoSelectStatic(TypedAppMixin, Static): """This class is used in window.py and windowbar.py to create buttons.""" @property diff --git a/src/textual_window/switcher.py b/src/textual_window/switcher.py index 5c65784..93771f9 100644 --- a/src/textual_window/switcher.py +++ b/src/textual_window/switcher.py @@ -23,14 +23,14 @@ # Local imports from textual_window.manager import window_manager -from textual_window.button_bases import ButtonStatic +from textual_window.button_bases import ButtonStatic, TypedAppMixin __all__ = [ "WindowSwitcher", ] -class WindowSwitcher(Widget): +class WindowSwitcher(TypedAppMixin, Widget): def __init__(self, cycle_key: str = "f1", **kwargs: Any) -> None: super().__init__(**kwargs) @@ -49,7 +49,7 @@ def __init__(self, name: str, content: VisualType = "") -> None: super().__init__(name=name, content=content) -class WindowSwitcherScreen(ModalScreen[None]): +class WindowSwitcherScreen(TypedAppMixin, ModalScreen[None]): CSS = """ WindowSwitcherScreen { align: center middle; } diff --git a/src/textual_window/window.py b/src/textual_window/window.py index 30a05a1..6e4e500 100644 --- a/src/textual_window/window.py +++ b/src/textual_window/window.py @@ -34,6 +34,7 @@ # Local imports from textual_window.manager import window_manager from textual_window.windowcomponents import Resizer, TopBar, BottomBar +from textual_window.button_bases import TypedAppMixin __all__ = [ "Window", @@ -79,7 +80,7 @@ def control(self) -> Window: return self.window -class Window(Widget): +class Window(TypedAppMixin, Widget): DEFAULT_CSS = """ Window { diff --git a/src/textual_window/windowbar.py b/src/textual_window/windowbar.py index 855a6f4..87243da 100644 --- a/src/textual_window/windowbar.py +++ b/src/textual_window/windowbar.py @@ -31,7 +31,7 @@ # Local imports from textual_window.manager import window_manager # The manager is a singleton instance -from textual_window.button_bases import NoSelectStatic, ButtonStatic +from textual_window.button_bases import NoSelectStatic, ButtonStatic, TypedAppMixin __all__ = [ "WindowBar", @@ -245,7 +245,7 @@ async def button_pressed(self, event: ButtonStatic.Pressed) -> None: self.window_bar.toggle_dock_location() -class WindowBar(Horizontal): +class WindowBar(TypedAppMixin, Horizontal): DEFAULT_CSS = """ WindowBar { diff --git a/tests/__snapshots__/test_suite/test_snapshot_launch_only.svg b/tests/__snapshots__/test_suite/test_snapshot_launch_only.svg index 609dd29..b886c76 100644 --- a/tests/__snapshots__/test_suite/test_snapshot_launch_only.svg +++ b/tests/__snapshots__/test_suite/test_snapshot_launch_only.svg @@ -163,7 +163,7 @@ - + ╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳ ╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳ diff --git a/tests/test_suite.py b/tests/test_suite.py index f28385e..13e40f8 100644 --- a/tests/test_suite.py +++ b/tests/test_suite.py @@ -1,18 +1,27 @@ from pathlib import Path +import pytest +from textual import __version__ as textual_version from textual.pilot import Pilot from textual_window.demo import WindowDemo + # from .app_test import WindowTestApp DEMO_DIR = Path(__file__).parent TERINAL_SIZE = (110, 36) -async def test_launch(): + +async def test_launch(): """Test launching the WindowDemo app.""" app = WindowDemo() async with app.run_test() as pilot: await pilot.pause() - await pilot.exit(None) + await pilot.exit(None) + +@pytest.mark.skipif( + int(textual_version.split(".")[0]) < 8, + reason="Snapshot baseline is generated under Textual 8.x; older Rich emits a different SVG format", +) def test_snapshot_launch_only(snap_compare): async def run_before(pilot: Pilot[None]) -> None: @@ -22,4 +31,4 @@ async def run_before(pilot: Pilot[None]) -> None: DEMO_DIR / "app_test.py", terminal_size=TERINAL_SIZE, run_before=run_before, - ) \ No newline at end of file + ) diff --git a/uv.lock b/uv.lock index 59dccbf..ebc122d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.10'", @@ -451,9 +451,6 @@ wheels = [ linkify = [ { name = "linkify-it-py" }, ] -plugins = [ - { name = "mdit-py-plugins" }, -] [[package]] name = "markupsafe" @@ -998,15 +995,15 @@ wheels = [ [[package]] name = "rich" -version = "14.1.0" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] @@ -1048,18 +1045,19 @@ wheels = [ [[package]] name = "textual" -version = "5.1.0" +version = "8.2.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", extra = ["linkify", "plugins"] }, + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, { name = "platformdirs" }, { name = "pygments" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/11/6d6cb61240dcf4f0ced928f5688302e4afe3fc6f2f20a39b0c90a6505fc8/textual-5.1.0.tar.gz", hash = "sha256:28e3833c8bef2319ab09483077c9812da422cecdeaa673319ba00f0523921e67", size = 28650804, upload-time = "2025-07-31T08:57:25.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/8c/1689f343776717800f620a4ce073087a62451ecdd37d211b1bfd2d575c7d/textual-5.1.0-py3-none-any.whl", hash = "sha256:6847df7e094c33bb9d1328e7173d0417fe27a4be63df09e394cc26584ad74a08", size = 14972224, upload-time = "2025-07-31T08:57:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, ] [[package]] @@ -1098,7 +1096,7 @@ wheels = [ [[package]] name = "textual-window" -version = "0.8.1" +version = "0.8.2" source = { editable = "." } dependencies = [ { name = "ezpubsub" }, @@ -1120,7 +1118,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "ezpubsub", specifier = ">=0.3.0" }, - { name = "textual", specifier = ">=5.1.0,<6.0.0" }, + { name = "textual", specifier = ">=5.1.0,<9.0.0" }, ] [package.metadata.requires-dev]