textual: Querying TabPane by ID returns multiple nodes

When assigning an ID to a TabPane, it seems that there is always a corresponding ContentTab created with the same ID, and therefore failing query_one(id) calls.

This discussion thread is relevant, and I can use the TabPane#foo trick to workaround this issue. But this still seems to be a bug to me.

Repro:

from textual.app import App, ComposeResult
from textual.widgets import TabbedContent, TabPane, Button


class ExampleApp(App):
    def compose(self) -> ComposeResult:
        with TabbedContent():
            yield TabPane("A", id="tab-a")

        yield Button("Repro")

    def on_button_pressed(self) -> None:
        for node in self.query("#tab-a"):
            self.log(f"### {node} ###")
        self.query_one("#tab-a")


if __name__ == "__main__":
    ExampleApp().run()

Clicking the button produces the following error:

TooManyMatches: Call to only_one resulted in more than one matched node

Relevant log lines in Textual console:

[01:05:37] INFO                                         tabbed_content_bug.py:14
### ContentTab(id='tab-a') ###
[01:05:37] INFO                                         tabbed_content_bug.py:14
### TabPane(id='tab-a') ###

Textual Diagnostics

Versions

Name Value
Textual 0.41.0
Rich 13.5.2

Python

Name Value
Version 3.11.2
Implementation CPython
Compiler Clang 15.0.7
Executable /Users/lian/local/src/textual/main/.venv/bin/python

Operating System

Name Value
System Darwin
Release 22.6.0
Version Darwin Kernel Version 22.6.0: Wed Jul 5 22:22:05 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T6000

Terminal

Name Value
Terminal Application tmux (3.3a)
TERM screen-256color
COLORTERM truecolor
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

Name Value
size width=80, height=23
legacy_windows False
min_width 1
max_width 80
is_terminal False
encoding utf-8
max_height 23
justify None
overflow None
no_wrap False
highlight None
markup None
height None

About this issue

  • Original URL
  • State: closed
  • Created 7 months ago
  • Comments: 19 (16 by maintainers)

Commits related to this issue

Most upvoted comments

Just realised there’s one huge gotcha here: the @on decorator and TabbedContent.TabActivated.tab (and more generally the tab property of TabbedContent.TabActivated). While a developer might wish that a query_one of a given TabPane ID result in just that TabPane and not the result that caused this re-issue, it would also seem reasonable that they’d expect to be able to write:

@on(TabbedContent.TabActivated, tab="#first-tab")

and have it do the right thing. In retrospect it makes more sense that TabbedContent didn’t have a TabActivated message but, instead, had a TabPaneActivated message.

Gonna need to think on this one.

  • “Unique” in opposition to “the same as the corresponding TabPane”.
  • “Which the user has no way of specifying” refers to the state of things currently. When we create a TabbedContent, the user has no control on how the ContentTab instances are created.

With these two 👆 clarifications, does my point make sense?

Hey @liancheng, thanks for the issue and for linking to the relevant discussion.

@willmcgugan, although this is “expected behaviour” in that we clearly implemented it this way, I think it makes no sense for us to cause this confusion to devs and to have queries break for no good reason.

As for a fix, something as simple as prefixing the internal IDs might cut it.