Skip to content

Commit 66a6448

Browse files
Prevent reactive-watcher loop in Tabs / TabbedContent. (#2305)
* Add regression test for #2229. * Fix potential reactive-watch loop. * Simplify regression test. Labels are cheaper to use and the final visual result of the test won't depend on the directory it runs from. * Simplify solution. Turns out I didn't need a descriptor. :( * Fail on empty tab.
1 parent 3a7cf08 commit 66a6448

File tree

5 files changed

+196
-20
lines changed

5 files changed

+196
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
3232
- Fix empty ListView preventing bindings from firing https://github.com/Textualize/textual/pull/2281
3333
- Fix `get_component_styles` returning incorrect values on first call when combined with pseudoclasses https://github.com/Textualize/textual/pull/2304
3434
- Fixed `active_message_pump.get` sometimes resulting in a `LookupError` https://github.com/Textualize/textual/issues/2301
35+
- Fixed issue arising when active tab was changed too quickly in succession https://github.com/Textualize/textual/pull/2305
3536

3637
## [0.19.1] - 2023-04-10
3738

src/textual/widgets/_tabbed_content.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,6 @@ class TabbedContent(Widget):
8383
}
8484
"""
8585

86-
active: reactive[str] = reactive("", init=False)
87-
"""The ID of the active tab, or empty string if none are active."""
88-
8986
class TabActivated(Message):
9087
"""Posted when the active tab changes."""
9188

@@ -116,21 +113,16 @@ def __init__(self, *titles: TextType, initial: str = "") -> None:
116113
self._initial = initial
117114
super().__init__()
118115

119-
def validate_active(self, active: str) -> str:
120-
"""It doesn't make sense for `active` to be an empty string.
121-
122-
Args:
123-
active: Attribute to be validated.
124-
125-
Returns:
126-
Value of `active`.
116+
@property
117+
def active(self) -> str:
118+
"""The ID of the active tab, or empty string if none are active."""
119+
return self.get_child_by_type(Tabs).active
127120

128-
Raises:
129-
ValueError: If the active attribute is set to empty string.
130-
"""
121+
@active.setter
122+
def active(self, active: str) -> None:
131123
if not active:
132124
raise ValueError("'active' tab must not be empty string.")
133-
return active
125+
self.get_child_by_type(Tabs).active = active
134126

135127
def compose(self) -> ComposeResult:
136128
"""Compose the tabbed content."""
@@ -186,7 +178,6 @@ def _on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:
186178
switcher = self.get_child_by_type(ContentSwitcher)
187179
assert isinstance(event.tab, ContentTab)
188180
switcher.current = event.tab.id
189-
self.active = event.tab.id
190181
self.post_message(
191182
TabbedContent.TabActivated(
192183
tabbed_content=self,
@@ -197,7 +188,3 @@ def _on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:
197188
def _on_tabs_cleared(self, event: Tabs.Cleared) -> None:
198189
"""All tabs were removed."""
199190
event.stop()
200-
201-
def watch_active(self, active: str) -> None:
202-
"""Switch tabs when the active attributes changes."""
203-
self.get_child_by_type(Tabs).active = active

tests/snapshot_tests/__snapshots__/test_snapshots.ambr

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19676,6 +19676,165 @@
1967619676

1967719677
'''
1967819678
# ---
19679+
# name: test_quickly_change_tabs
19680+
'''
19681+
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
19682+
<!-- Generated with Rich https://www.textualize.io -->
19683+
<style>
19684+
19685+
@font-face {
19686+
font-family: "Fira Code";
19687+
src: local("FiraCode-Regular"),
19688+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
19689+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
19690+
font-style: normal;
19691+
font-weight: 400;
19692+
}
19693+
@font-face {
19694+
font-family: "Fira Code";
19695+
src: local("FiraCode-Bold"),
19696+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
19697+
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
19698+
font-style: bold;
19699+
font-weight: 700;
19700+
}
19701+
19702+
.terminal-1586716314-matrix {
19703+
font-family: Fira Code, monospace;
19704+
font-size: 20px;
19705+
line-height: 24.4px;
19706+
font-variant-east-asian: full-width;
19707+
}
19708+
19709+
.terminal-1586716314-title {
19710+
font-size: 18px;
19711+
font-weight: bold;
19712+
font-family: arial;
19713+
}
19714+
19715+
.terminal-1586716314-r1 { fill: #c5c8c6 }
19716+
.terminal-1586716314-r2 { fill: #737373 }
19717+
.terminal-1586716314-r3 { fill: #e1e1e1;font-weight: bold }
19718+
.terminal-1586716314-r4 { fill: #323232 }
19719+
.terminal-1586716314-r5 { fill: #0178d4 }
19720+
.terminal-1586716314-r6 { fill: #e1e1e1 }
19721+
</style>
19722+
19723+
<defs>
19724+
<clipPath id="terminal-1586716314-clip-terminal">
19725+
<rect x="0" y="0" width="975.0" height="584.5999999999999" />
19726+
</clipPath>
19727+
<clipPath id="terminal-1586716314-line-0">
19728+
<rect x="0" y="1.5" width="976" height="24.65"/>
19729+
</clipPath>
19730+
<clipPath id="terminal-1586716314-line-1">
19731+
<rect x="0" y="25.9" width="976" height="24.65"/>
19732+
</clipPath>
19733+
<clipPath id="terminal-1586716314-line-2">
19734+
<rect x="0" y="50.3" width="976" height="24.65"/>
19735+
</clipPath>
19736+
<clipPath id="terminal-1586716314-line-3">
19737+
<rect x="0" y="74.7" width="976" height="24.65"/>
19738+
</clipPath>
19739+
<clipPath id="terminal-1586716314-line-4">
19740+
<rect x="0" y="99.1" width="976" height="24.65"/>
19741+
</clipPath>
19742+
<clipPath id="terminal-1586716314-line-5">
19743+
<rect x="0" y="123.5" width="976" height="24.65"/>
19744+
</clipPath>
19745+
<clipPath id="terminal-1586716314-line-6">
19746+
<rect x="0" y="147.9" width="976" height="24.65"/>
19747+
</clipPath>
19748+
<clipPath id="terminal-1586716314-line-7">
19749+
<rect x="0" y="172.3" width="976" height="24.65"/>
19750+
</clipPath>
19751+
<clipPath id="terminal-1586716314-line-8">
19752+
<rect x="0" y="196.7" width="976" height="24.65"/>
19753+
</clipPath>
19754+
<clipPath id="terminal-1586716314-line-9">
19755+
<rect x="0" y="221.1" width="976" height="24.65"/>
19756+
</clipPath>
19757+
<clipPath id="terminal-1586716314-line-10">
19758+
<rect x="0" y="245.5" width="976" height="24.65"/>
19759+
</clipPath>
19760+
<clipPath id="terminal-1586716314-line-11">
19761+
<rect x="0" y="269.9" width="976" height="24.65"/>
19762+
</clipPath>
19763+
<clipPath id="terminal-1586716314-line-12">
19764+
<rect x="0" y="294.3" width="976" height="24.65"/>
19765+
</clipPath>
19766+
<clipPath id="terminal-1586716314-line-13">
19767+
<rect x="0" y="318.7" width="976" height="24.65"/>
19768+
</clipPath>
19769+
<clipPath id="terminal-1586716314-line-14">
19770+
<rect x="0" y="343.1" width="976" height="24.65"/>
19771+
</clipPath>
19772+
<clipPath id="terminal-1586716314-line-15">
19773+
<rect x="0" y="367.5" width="976" height="24.65"/>
19774+
</clipPath>
19775+
<clipPath id="terminal-1586716314-line-16">
19776+
<rect x="0" y="391.9" width="976" height="24.65"/>
19777+
</clipPath>
19778+
<clipPath id="terminal-1586716314-line-17">
19779+
<rect x="0" y="416.3" width="976" height="24.65"/>
19780+
</clipPath>
19781+
<clipPath id="terminal-1586716314-line-18">
19782+
<rect x="0" y="440.7" width="976" height="24.65"/>
19783+
</clipPath>
19784+
<clipPath id="terminal-1586716314-line-19">
19785+
<rect x="0" y="465.1" width="976" height="24.65"/>
19786+
</clipPath>
19787+
<clipPath id="terminal-1586716314-line-20">
19788+
<rect x="0" y="489.5" width="976" height="24.65"/>
19789+
</clipPath>
19790+
<clipPath id="terminal-1586716314-line-21">
19791+
<rect x="0" y="513.9" width="976" height="24.65"/>
19792+
</clipPath>
19793+
<clipPath id="terminal-1586716314-line-22">
19794+
<rect x="0" y="538.3" width="976" height="24.65"/>
19795+
</clipPath>
19796+
</defs>
19797+
19798+
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-1586716314-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">QuicklyChangeTabsApp</text>
19799+
<g transform="translate(26,22)">
19800+
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
19801+
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
19802+
<circle cx="44" cy="0" r="7" fill="#28c840"/>
19803+
</g>
19804+
19805+
<g transform="translate(9, 41)" clip-path="url(#terminal-1586716314-clip-terminal)">
19806+
<rect fill="#1e1e1e" x="0" y="1.5" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="73.2" y="1.5" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="146.4" y="1.5" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="244" y="1.5" width="732" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="24.4" y="25.9" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="61" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="73.2" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="97.6" y="25.9" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="134.2" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="146.4" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="25.9" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="231.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="244" y="25.9" width="732" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="158.6" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="170.8" y="50.3" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="231.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="244" y="50.3" width="732" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="24.4" y="99.1" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="85.4" y="99.1" width="866.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="951.6" y="99.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
19807+
<g class="terminal-1586716314-matrix">
19808+
<text class="terminal-1586716314-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1586716314-line-0)">
19809+
</text><text class="terminal-1586716314-r2" x="24.4" y="44.4" textLength="36.6" clip-path="url(#terminal-1586716314-line-1)">one</text><text class="terminal-1586716314-r2" x="97.6" y="44.4" textLength="36.6" clip-path="url(#terminal-1586716314-line-1)">two</text><text class="terminal-1586716314-r3" x="170.8" y="44.4" textLength="61" clip-path="url(#terminal-1586716314-line-1)">three</text><text class="terminal-1586716314-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1586716314-line-1)">
19810+
</text><text class="terminal-1586716314-r4" x="0" y="68.8" textLength="158.6" clip-path="url(#terminal-1586716314-line-2)">━━━━━━━━━━━━━</text><text class="terminal-1586716314-r4" x="158.6" y="68.8" textLength="12.2" clip-path="url(#terminal-1586716314-line-2)">╸</text><text class="terminal-1586716314-r5" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-1586716314-line-2)">━━━━━</text><text class="terminal-1586716314-r4" x="231.8" y="68.8" textLength="12.2" clip-path="url(#terminal-1586716314-line-2)">╺</text><text class="terminal-1586716314-r4" x="244" y="68.8" textLength="732" clip-path="url(#terminal-1586716314-line-2)">━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</text><text class="terminal-1586716314-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1586716314-line-2)">
19811+
</text><text class="terminal-1586716314-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1586716314-line-3)">
19812+
</text><text class="terminal-1586716314-r6" x="24.4" y="117.6" textLength="61" clip-path="url(#terminal-1586716314-line-4)">three</text><text class="terminal-1586716314-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1586716314-line-4)">
19813+
</text><text class="terminal-1586716314-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1586716314-line-5)">
19814+
</text><text class="terminal-1586716314-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1586716314-line-6)">
19815+
</text><text class="terminal-1586716314-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1586716314-line-7)">
19816+
</text><text class="terminal-1586716314-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1586716314-line-8)">
19817+
</text><text class="terminal-1586716314-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1586716314-line-9)">
19818+
</text><text class="terminal-1586716314-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1586716314-line-10)">
19819+
</text><text class="terminal-1586716314-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1586716314-line-11)">
19820+
</text><text class="terminal-1586716314-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1586716314-line-12)">
19821+
</text><text class="terminal-1586716314-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1586716314-line-13)">
19822+
</text><text class="terminal-1586716314-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1586716314-line-14)">
19823+
</text><text class="terminal-1586716314-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1586716314-line-15)">
19824+
</text><text class="terminal-1586716314-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1586716314-line-16)">
19825+
</text><text class="terminal-1586716314-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1586716314-line-17)">
19826+
</text><text class="terminal-1586716314-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1586716314-line-18)">
19827+
</text><text class="terminal-1586716314-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1586716314-line-19)">
19828+
</text><text class="terminal-1586716314-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1586716314-line-20)">
19829+
</text><text class="terminal-1586716314-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1586716314-line-21)">
19830+
</text><text class="terminal-1586716314-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1586716314-line-22)">
19831+
</text>
19832+
</g>
19833+
</g>
19834+
</svg>
19835+
19836+
'''
19837+
# ---
1967919838
# name: test_radio_button_example
1968019839
'''
1968119840
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Regression test for https://github.com/Textualize/textual/issues/2229."""
2+
from textual.app import App, ComposeResult
3+
from textual.widgets import TabbedContent, TabPane, Tabs, Label
4+
5+
6+
class QuicklyChangeTabsApp(App[None]):
7+
def compose(self) -> ComposeResult:
8+
with TabbedContent():
9+
with TabPane("one"):
10+
yield Label("one")
11+
with TabPane("two"):
12+
yield Label("two")
13+
with TabPane("three", id="three"):
14+
yield Label("three")
15+
16+
def key_p(self) -> None:
17+
self.query_one(Tabs).action_next_tab()
18+
self.query_one(Tabs).action_next_tab()
19+
20+
21+
app = QuicklyChangeTabsApp()
22+
23+
if __name__ == "__main__":
24+
app.run()

tests/snapshot_tests/test_snapshots.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,8 @@ def test_scroll_to_center(snap_compare):
431431
# scrolled so that the red string >>bullseye<< is centered on the screen.
432432
# When this snapshot "breaks" because #2254 is fixed, this snapshot can be updated.
433433
assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_to_center.py", press=["s"])
434+
435+
436+
def test_quickly_change_tabs(snap_compare):
437+
# https://github.com/Textualize/textual/issues/2229
438+
assert snap_compare(SNAPSHOT_APPS_DIR / "quickly_change_tabs.py", press=["p"])

0 commit comments

Comments
 (0)