Textualize / textual

The lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.
https://textual.textualize.io/
MIT License
25.1k stars 766 forks source link

SelectionList doesn't actually allow Rich renderables #4361

Closed goblin closed 5 months ago

goblin commented 5 months ago

Issue

When trying to use a Rich Renderable, in form of a Rich Table, as a prompt for SelectionList like in this sample app:

from textual.app import App
from textual.widgets import SelectionList
from rich.table import Table

tab = Table("Head1", "Head2")
tab.add_row('item1', 'item2')

class MyApp(App):
    def compose(self):
        yield SelectionList((tab, 1))

MyApp().run()

I'm getting an exception, and then another one:

Traceback (most recent call last):
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 2424, in _process_messages
    await run_process_messages()
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 2365, in run_process_messages
    await self._dispatch_message(events.Compose())
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/message_pump.py", line 634, in _dispatch_message
    await self.on_event(message)
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 2860, in on_event
    await super().on_event(event)
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/message_pump.py", line 703, in on_event
    await self._on_message(event)
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/message_pump.py", line 724, in _on_message
    await invoke(method, message)
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/_callback.py", line 85, in invoke
    return await _invoke(callback, *params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/_callback.py", line 47, in _invoke
    result = await result
             ^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 2460, in _on_compose
    widgets = [*self.screen._nodes, *compose(self)]
                                     ^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/_compose.py", line 33, in compose
    child = next(iter_compose)
            ^^^^^^^^^^^^^^^^^^
  File "/home/goblin/git/proj/c.py", line 10, in compose
    yield SelectionList((tab, 1))
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/widgets/_selection_list.py", line 256, in __init__
    options = [self._make_selection(selection) for selection in selections]
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/widgets/_selection_list.py", line 256, in <listcomp>
    options = [self._make_selection(selection) for selection in selections]
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/widgets/_selection_list.py", line 495, in _make_selection
    selection = Selection[SelectionType](*selection)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/typing.py", line 1248, in __call__
    result = self.__origin__(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/widgets/_selection_list.py", line 54, in __init__
    super().__init__(prompt.split()[0], id, disabled)
                     ^^^^^^^^^^^^
AttributeError: 'Table' object has no attribute 'split'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/goblin/git/proj/c.py", line 12, in <module>
    MyApp().run()
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 1550, in run
    asyncio.run(run_app())
  File "/usr/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 1539, in run_app
    await self.run_async(
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 1501, in run_async
    await app._process_messages(
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 2429, in _process_messages
    self._handle_exception(error)
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 2261, in _handle_exception
    self._fatal_error()
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/app.py", line 2268, in _fatal_error
    traceback = Traceback(
                ^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/rich/traceback.py", line 264, in __init__
    trace = self.extract(
            ^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/rich/traceback.py", line 449, in extract
    locals={
           ^
  File "/home/goblin/venv/lib/python3.11/site-packages/rich/traceback.py", line 450, in <dictcomp>
    key: pretty.traverse(
         ^^^^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/rich/pretty.py", line 852, in traverse
    node = _traverse(_object, root=True)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/rich/pretty.py", line 647, in _traverse
    args = list(iter_rich_args(rich_repr_result))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/rich/pretty.py", line 614, in iter_rich_args
    for arg in rich_args:
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/widgets/_option_list.py", line 74, in __rich_repr__
    yield "prompt", self.prompt
                    ^^^^^^^^^^^
  File "/home/goblin/venv/lib/python3.11/site-packages/textual/widgets/_option_list.py", line 58, in prompt
    return self.__prompt
           ^^^^^^^^^^^^^
AttributeError: 'Selection' object has no attribute '_Option__prompt'

While it may not make sense to use a multi-line Table as a SelectionList Option, it definitely shouldn't cause two exceptions like this. It also shows that under the hood, SelectionList actually expects Text, not just any Renderable.

Documentation

According to the docs, Rich Renderables should be allowed as SelectionList Options:

https://textual.textualize.io/widgets/selection_list/#examples:

A selection list is designed to be built up of single-line prompts (which can be Rich renderables)

When navigating to the Rich renderables link, we actually see Rich Table used as an example of a Rich Renderable, along with the fizzbuzz example.

Textual Diagnostics

Versions

Name Value
Textual 0.54.0
Rich 13.4.2

Python

Name Value
Version 3.11.2
Implementation CPython
Compiler GCC 12.2.0
Executable /home/goblin/venv/bin/python3

Operating System

Name Value
System Linux
Release 6.1.0-11-amd64
Version #1 SMP PREEMPT_DYNAMIC Debian 6.1.38-4 (2023-08-08)

Terminal

Name Value
Terminal Application Unknown
TERM xterm-256color
COLORTERM truecolor
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

Name Value
size width=121, height=31
legacy_windows False
min_width 1
max_width 121
is_terminal True
encoding utf-8
max_height 31
justify None
overflow None
no_wrap False
highlight None
markup None
height None
davep commented 5 months ago

This would seem to be unfortunate wording in the docs. If I recall correctly what is supported is TextType, which is Text | str. So you can use a Text Rich renderable, or a string.

goblin commented 5 months ago

Yup, that's OK.

It's a bit of a pity though, DataTable seems to work fine with more generic Renderables (and shows the first row of the Table in this case):

from textual.app import App
from textual.widgets import DataTable
from rich.table import Table

class MyApp(App):
    def compose(self):
        subtab = Table('sub1', 'sub2')
        subtab.add_row('it1', 'it2')

        tab = DataTable()
        tab.add_columns("Head1", "Head2")
        tab.add_row(subtab, 'Item2')

        yield tab

MyApp().run()

I'm trying to apply customization of the SelectionList Options based on the available width of the list, and it's tricky to do if Renderables aren't allowed.

github-actions[bot] commented 5 months ago

Don't forget to star the repository!

Follow @textualizeio for Textual updates.