diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index c586e6909f..7e38b99967 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -637,7 +637,6 @@ def apply( cache_key: tuple | None = None path_key: tuple | None = None - css_path_nodes: list[DOMNode] | None = None if cache is not None and all_pseudo_classes.isdisjoint( self._EXCLUDE_PSEUDO_CLASSES_FROM_CACHE @@ -660,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 path_key = ( "css_path", rule_key, @@ -681,9 +688,6 @@ def apply( self._process_component_classes(node) return - if css_path_nodes is None: - css_path_nodes = node.css_path_nodes - # Rules that may be set to the special value `initial` initial: set[str] = set() # Rules in DEFAULT_CSS set to the special value `initial` 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: