From abb5cef0238cb0894bebe6eda0652e72fa63073e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Wed, 7 Oct 2026 09:37:16 -0400 Subject: [PATCH 1/2] Acquire CSS ancestry only when current selector declarations require it --- src/textual/css/stylesheet.py | 9 +++++++-- tests/css/test_stylesheet.py | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index c586e6909f..7f5c2a84ef 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -638,6 +638,11 @@ def apply( cache_key: tuple | None = None path_key: tuple | None = None css_path_nodes: list[DOMNode] | None = None + # Matching already declares whether a group addresses its target + # alone. Read that live declaration: selector lists remain editable. + requires_path = any( + not group.is_compound for rule in rules for group in rule.selector_set + ) if cache is not None and all_pseudo_classes.isdisjoint( self._EXCLUDE_PSEUDO_CLASSES_FROM_CACHE @@ -665,7 +670,7 @@ def apply( # cannot distinguish these paths when positional/focus-within # selectors were excluded above. Share resolved rules within this # update batch rather than matching every one from scratch. - css_path_nodes = node.css_path_nodes + css_path_nodes = node.css_path_nodes if requires_path else [node] path_key = ( "css_path", rule_key, @@ -682,7 +687,7 @@ def apply( return if css_path_nodes is None: - css_path_nodes = node.css_path_nodes + css_path_nodes = node.css_path_nodes if requires_path else [node] # Rules that may be set to the special value `initial` initial: set[str] = set() diff --git a/tests/css/test_stylesheet.py b/tests/css/test_stylesheet.py index 01e378ff27..f437f935ba 100644 --- a/tests/css/test_stylesheet.py +++ b/tests/css/test_stylesheet.py @@ -18,6 +18,32 @@ from textual.widgets import Static +def test_target_rule_publication_acquires_only_declared_css_path(): + class PathReads(DOMNode): + path_reads = 0 + + @property + def css_path_nodes(self): + self.path_reads += 1 + return super().css_path_nodes + + parent = DOMNode(classes="outer") + node = PathReads(classes="leaf") + node._attach(parent) + stylesheet = Stylesheet() + stylesheet.add_source(".leaf { color: red; }") + stylesheet.apply(node) + assert node.styles.color == Color.parse("red") + assert node.path_reads == 0 + + # Reparse a real child declaration: full ancestry and specificity must + # become authoritative again, not the previous target-only key. + stylesheet.add_source(".outer > .leaf { color: blue; }") + stylesheet.apply(node) + assert node.styles.color == Color.parse("blue") + assert node.path_reads == 1 + + async def test_equal_current_rules_retarget_a_pending_transition(): app = App() async with app.run_test() as pilot: From 6bf44a984aa7f786be7643af092c71314b2ebafb Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Wed, 7 Oct 2026 09:38:40 -0400 Subject: [PATCH 2/2] Acquire one declared CSS path only after the original cache miss --- src/textual/css/stylesheet.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index 7f5c2a84ef..7e38b99967 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -637,12 +637,6 @@ def apply( cache_key: tuple | None = None path_key: tuple | None = None - css_path_nodes: list[DOMNode] | None = None - # Matching already declares whether a group addresses its target - # alone. Read that live declaration: selector lists remain editable. - requires_path = any( - not group.is_compound for rule in rules for group in rule.selector_set - ) if cache is not None and all_pseudo_classes.isdisjoint( self._EXCLUDE_PSEUDO_CLASSES_FROM_CACHE @@ -665,12 +659,20 @@ def apply( self._process_component_classes(node) return + # Matching already declares whether a group addresses its target + # alone. Read the live declaration only after the first cache miss; + # selector lists remain editable and hits need no path planning. + requires_path = any( + not group.is_compound for rule in rules for group in rule.selector_set + ) + css_path_nodes = node.css_path_nodes if requires_path else [node] + + if cache_key is not None: # Widgets in repeated rows have distinct parent *instances* but # frequently the same selector/pseudo-class ancestry. A CSS rule # cannot distinguish these paths when positional/focus-within # selectors were excluded above. Share resolved rules within this # update batch rather than matching every one from scratch. - css_path_nodes = node.css_path_nodes if requires_path else [node] path_key = ( "css_path", rule_key, @@ -686,9 +688,6 @@ def apply( self._process_component_classes(node) return - if css_path_nodes is None: - css_path_nodes = node.css_path_nodes if requires_path else [node] - # Rules that may be set to the special value `initial` initial: set[str] = set() # Rules in DEFAULT_CSS set to the special value `initial`