The bug
I've optimized uproot-browser recently (using Claude Fable during the brief window it was available), and it works really well, but there's an occasional crash on CI - always on Windows. I've looked into it, and put the summary below - the best way to reduce the issue seems to go back to not lazy-loading the tools tab anymore (in scikit-hep/uproot-browser#249). It's a pretty rare crash, about 1/3 of the time and there are several windows jobs, so maybe 1/9? I think it's due to speed, though, not Windows specific, as Claude was able to make a MWE that I can see crash.
Happy to try applying this as a PR if you want me to.
🤖 Claude Opus issue report 🤖
Title: Select can crash during mount: SelectCurrent.update() queries #label before it is mounted (NoMatches)
Textual version: 8.2.7
Platform: Windows (GitHub Actions windows-latest), Python 3.10/3.12 — intermittent, timing-dependent
Description
Select._on_mount eagerly initializes the value, which cascades into a query for a child that may not be mounted yet, crashing the mount:
Select._on_mount
→ _init_selected_option(self._value)
→ self.value = hint
→ Select._watch_value
→ SelectCurrent.update(prompt)
→ self.query_one("#label", Static) # NoMatches
Select._watch_value defensively guards the outer lookup:
try:
select_current = self.query_one(SelectCurrent)
except NoMatches:
pass
…but SelectCurrent.update() itself does an unguarded inner query:
def update(self, label: RenderableType | NoSelection) -> None:
self.label = label
self.has_value = label is not Select.NULL
self.query_one("#label", Static).update(...) # crashes if #label not mounted yet
So when SelectCurrent is mounted but its own composed children (#label, from SelectCurrent.compose) are not yet, update() raises NoMatches and the Select fails to finish mounting (its value stays Select.NULL).
Traceback (abridged)
.../textual/widgets/_select.py:629 in _on_mount
self._init_selected_option(self._value)
.../textual/widgets/_select.py:557 in _init_selected_option
self.value = hint # hint = 'textual-dark'
.../textual/widgets/_select.py:615 in _watch_value
select_current.update(prompt)
.../textual/widgets/_select.py:256 in update
self.query_one("#label", Static).update(...)
.../textual/dom.py:1505: NoMatches
NoMatches: No nodes match '#label' on SelectCurrent(classes='-has-value')
When it shows up
Intermittently on slow CI runners, and notably when the Select is mounted via textual.lazy.Lazy(...), which changes mount ordering. (allow_blank=False with an explicit value= guarantees _init_selected_option sets a value during mount, so the path is always exercised.)
Suggested fix
Make SelectCurrent.update() resilient to its children not yet being mounted — either guard the #label lookup the same way _watch_value guards SelectCurrent, or defer the update until after SelectCurrent has fully mounted. For example:
def update(self, label: RenderableType | NoSelection) -> None:
self.label = label
self.has_value = label is not Select.NULL
try:
label_widget = self.query_one("#label", Static)
except NoMatches:
return # not mounted yet; _on_mount will render the initial value
label_widget.update(
self.placeholder if isinstance(label, NoSelection) else label
)
This happens on Windows in CI, so I didn't include textual diagnose output.
🤖 MWE 🤖 (verified with a manual run too)
"""MWE: Textual `Select` crashes during mount with `NoMatches` on '#label'.
`Select._watch_value` guards `query_one(SelectCurrent)` with try/except NoMatches,
but `SelectCurrent.update()` then does an UNGUARDED `query_one("#label", Static)`.
There is a window during mount where `SelectCurrent` is already mounted (so the
guard passes) yet its composed '#label' child is not mounted yet. The value-init
in `Select._on_mount` lands in that window and crashes:
Select._on_mount
-> _init_selected_option(self._value) # allow_blank=False + value
-> self.value = ...
-> Select._watch_value # query_one(SelectCurrent) OK
-> SelectCurrent.update(...)
-> self.query_one("#label", Static) # NoMatches <-- bug
In real apps this is an intermittent race on slow runners (seen on Windows CI,
and made more likely by mounting the Select via `textual.lazy.Lazy`). Here it is
made deterministic by deferring ONLY the '#label' child's mount by one refresh,
so `Select._on_mount` reliably falls inside the window. Everything else is stock.
Tested with Textual 8.2.7. Run with `python textual_select_mount_mwe.py`; it
exits with the NoMatches traceback above (remove SlowSelect to confirm the
stock Select usually does NOT crash on a fast machine).
"""
import textual.app
import textual.widgets as w
from textual.widgets._select import NonSelectableStatic, SelectCurrent, SelectOverlay
class SlowSelectCurrent(SelectCurrent):
"""Stock SelectCurrent, but '#label' mounts one refresh late."""
def compose(self):
yield NonSelectableStatic("▼", classes="arrow down-arrow")
yield NonSelectableStatic("▲", classes="arrow up-arrow")
def on_mount(self):
# SelectCurrent is mounted now (the _watch_value guard will pass), but
# '#label' is still pending -> reproduces the slow-runner ordering.
self.call_after_refresh(
self.mount, NonSelectableStatic(self.placeholder, id="label")
)
class SlowSelect(w.Select):
def compose(self):
yield SlowSelectCurrent(self.prompt)
yield SelectOverlay(type_to_search=self._type_to_search).data_bind(
compact=w.Select.compact
)
class MWEApp(textual.app.App):
def compose(self):
yield SlowSelect([("a", "a"), ("b", "b")], allow_blank=False, value="a")
if __name__ == "__main__":
MWEApp().run()
The bug
I've optimized uproot-browser recently (using Claude Fable during the brief window it was available), and it works really well, but there's an occasional crash on CI - always on Windows. I've looked into it, and put the summary below - the best way to reduce the issue seems to go back to not lazy-loading the tools tab anymore (in scikit-hep/uproot-browser#249). It's a pretty rare crash, about 1/3 of the time and there are several windows jobs, so maybe 1/9? I think it's due to speed, though, not Windows specific, as Claude was able to make a MWE that I can see crash.
Happy to try applying this as a PR if you want me to.
🤖 Claude Opus issue report 🤖
Title:
Selectcan crash during mount:SelectCurrent.update()queries#labelbefore it is mounted (NoMatches)Textual version: 8.2.7
Platform: Windows (GitHub Actions
windows-latest), Python 3.10/3.12 — intermittent, timing-dependentDescription
Select._on_mounteagerly initializes the value, which cascades into a query for a child that may not be mounted yet, crashing the mount:Select._watch_valuedefensively guards the outer lookup:…but
SelectCurrent.update()itself does an unguarded inner query:So when
SelectCurrentis mounted but its own composed children (#label, fromSelectCurrent.compose) are not yet,update()raisesNoMatchesand theSelectfails to finish mounting (its value staysSelect.NULL).Traceback (abridged)
When it shows up
Intermittently on slow CI runners, and notably when the
Selectis mounted viatextual.lazy.Lazy(...), which changes mount ordering. (allow_blank=Falsewith an explicitvalue=guarantees_init_selected_optionsets a value during mount, so the path is always exercised.)Suggested fix
Make
SelectCurrent.update()resilient to its children not yet being mounted — either guard the#labellookup the same way_watch_valueguardsSelectCurrent, or defer the update until afterSelectCurrenthas fully mounted. For example:This happens on Windows in CI, so I didn't include
textual diagnoseoutput.🤖 MWE 🤖 (verified with a manual run too)