From f5d71ca513dc1d646145e08c3d3c6c666b7ad6fb Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Mar 2026 15:59:17 +0000 Subject: [PATCH 1/5] weakref node --- src/textual/css/styles.py | 16 +++++++++++++--- src/textual/dom.py | 4 +++- tests/css/test_styles.py | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 22eb4fae3c..6bf8befa50 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -1,5 +1,6 @@ from __future__ import annotations +import weakref from dataclasses import dataclass, field from functools import partial from operator import attrgetter @@ -256,8 +257,6 @@ class StylesBase: "line_pad", } - node: DOMNode | None = None - display = StringEnumProperty(VALID_DISPLAY, "block", layout=True, display=True) """Set the display of the widget, defining how it's rendered. @@ -519,6 +518,10 @@ class StylesBase: Requires terminal support for Kitty pointer shapes protocol. """ + @property + def node(self) -> DOMNode | None: + return None + def __textual_animation__( self, attribute: str, @@ -1331,7 +1334,7 @@ class RenderStyles(StylesBase): """Presents a combined view of two Styles object: a base Styles and inline Styles.""" def __init__(self, node: DOMNode, base: Styles, inline_styles: Styles) -> None: - self.node = node + self._node = weakref.ref(node) self._base_styles = base self._inline_styles = inline_styles self._animate: BoundAnimator | None = None @@ -1339,6 +1342,9 @@ def __init__(self, node: DOMNode, base: Styles, inline_styles: Styles) -> None: self._rich_style: tuple[int, Style] | None = None self._gutter: tuple[int, Spacing] | None = None + def _update_node(self, node: DOMNode) -> None: + self._node = weakref.ref(node) + def __eq__(self, other: object) -> bool: if isinstance(other, RenderStyles): return ( @@ -1356,6 +1362,10 @@ def _cache_key(self) -> int: """ return self._updates + self._base_styles._updates + self._inline_styles._updates + @property + def node(self) -> DOMNode | None: + return self._node() + @property def base(self) -> Styles: """Quick access to base (css) style.""" diff --git a/src/textual/dom.py b/src/textual/dom.py index 1687f45abc..8e6f1c6b55 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -615,7 +615,9 @@ def get_component_styles(self, *names: str) -> RenderStyles: if name not in self._component_styles: raise KeyError(f"No {name!r} key in COMPONENT_CLASSES") component_styles = self._component_styles[name] - styles.node = component_styles.node + assert component_styles.node is not None + styles._update_node(component_styles.node) + # styles.node = component_styles.node styles.base.merge(component_styles.base) styles.inline.merge(component_styles.inline) styles._updates += 1 diff --git a/tests/css/test_styles.py b/tests/css/test_styles.py index 2ead395a72..cb50406755 100644 --- a/tests/css/test_styles.py +++ b/tests/css/test_styles.py @@ -90,7 +90,7 @@ def test_merge_rules(): def test_render_styles_border(): base = Styles() inline = Styles() - styles_view = RenderStyles(None, base, inline) + styles_view = RenderStyles(DOMNode(), base, inline) base.border_top = ("heavy", "red") # Base has border-top: heavy red From b72bb27d149e84e82d34d75649c07fd9b2a8c190 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Mar 2026 16:10:17 +0000 Subject: [PATCH 2/5] weakrefs for markdowns --- src/textual/widgets/_markdown.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index c5bea8d4d0..b4d7419d83 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -2,6 +2,7 @@ import asyncio import re +import weakref from contextlib import suppress from functools import partial from pathlib import Path, PurePath @@ -210,7 +211,7 @@ def __init__( *args, **kwargs, ) -> None: - self._markdown: Markdown = markdown + self._markdown_ref = weakref.ref(markdown) """A reference to the Markdown document that contains this block.""" self._content: Content = Content() self._token: Token = token @@ -224,6 +225,12 @@ def __init__( *args, name=token.type, classes=f"level-{token.level}", **kwargs ) + @property + def _markdown(self) -> Markdown: + markdown = self._markdown_ref() + assert markdown is not None + return markdown + @property def select_container(self) -> Widget: return self.query_ancestor(Markdown) From 01ee769f39a3aed72d819b6bd7591004bf601aa5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Mar 2026 18:27:46 +0000 Subject: [PATCH 3/5] docstrings --- src/textual/css/styles.py | 2 ++ src/textual/widgets/_markdown.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 6bf8befa50..3374735690 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -520,6 +520,7 @@ class StylesBase: @property def node(self) -> DOMNode | None: + """The DOM node the styles will be applied to, or `None` if it is not set.""" return None def __textual_animation__( @@ -1364,6 +1365,7 @@ def _cache_key(self) -> int: @property def node(self) -> DOMNode | None: + """The DOM node the styles will be applied to, or `None` if it is not set.""" return self._node() @property diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index b4d7419d83..2b4f74a0ff 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -227,6 +227,7 @@ def __init__( @property def _markdown(self) -> Markdown: + """Resolve the weak ref to _markdown""" markdown = self._markdown_ref() assert markdown is not None return markdown From 0f7f8427365dd0916b7cf859de65d15af0e6d91b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Mar 2026 18:33:23 +0000 Subject: [PATCH 4/5] bump --- CHANGELOG.md | 10 ++++++++++ src/textual/css/styles.py | 5 +++++ src/textual/dom.py | 1 - 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb74d89f78..eb6beef6b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [8.0.1] - 2026-03-01 + +### Fixed + +- `DirectoryTree` runs more operations in a thread to avoid micro-freezes + +### Changes + +- Some tweaks to garbage collection to reduce gc time + ## [8.0.0] - 2026-02-16 ### Added diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 3374735690..12299a96d0 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -1344,6 +1344,11 @@ def __init__(self, node: DOMNode, base: Styles, inline_styles: Styles) -> None: self._gutter: tuple[int, Spacing] | None = None def _update_node(self, node: DOMNode) -> None: + """Update the associated DOM node. + + Args: + node: New node for the styles. + """ self._node = weakref.ref(node) def __eq__(self, other: object) -> bool: diff --git a/src/textual/dom.py b/src/textual/dom.py index 8e6f1c6b55..5fcf49e866 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -617,7 +617,6 @@ def get_component_styles(self, *names: str) -> RenderStyles: component_styles = self._component_styles[name] assert component_styles.node is not None styles._update_node(component_styles.node) - # styles.node = component_styles.node styles.base.merge(component_styles.base) styles.inline.merge(component_styles.inline) styles._updates += 1 From bc468e6b19c9755c74029f3af7a646bd5781d878 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Mar 2026 18:34:06 +0000 Subject: [PATCH 5/5] bump --- CHANGELOG.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb6beef6b1..af0bd95ceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3360,6 +3360,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[8.0.1]: https://github.com/Textualize/textual/compare/v8.0.0...v8.0.1 [8.0.0]: https://github.com/Textualize/textual/compare/v7.5.0...v8.0.0 [7.5.0]: https://github.com/Textualize/textual/compare/v7.4.0...v7.5.0 [7.4.0]: https://github.com/Textualize/textual/compare/v7.3.0...v7.4.0 diff --git a/pyproject.toml b/pyproject.toml index 146a4d53c7..a35bc698b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "8.0.0" +version = "8.0.1" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/"