textual
textual copied to clipboard
Subscripted generics cannot be used in query selectors
(I wasn't sure if a bug or feature request was more appropriate, sorry!)
Description
The Select
widget is generic over the type of the option values.
- we can use
Select[SomeType]
in e.g., a widget'scompose
method - we can't use
self.query_one(Select[SomeType])
, though; this crashes at runtime
This happens because the isinstance
check that Textual does is incompatible with subscripted generics (it's the same reason that isinstance(x, list[int])
doesn't work, which is detailed in PEP 585) .
This might be a bug, because:
- the API reference for
query_one
doesn't indicate that this won't work - the code type checks, but fails at runtime, which indicates that the type annotation is too broad
This is probably a feature request, though, because:
- generics have a ton of sharp edges
- it's very clear why this is happening
- it's not clear how Textual can/should handle this
Details
Here's a video, example, and diagnostics.
https://github.com/Textualize/textual/assets/11623486/540b6d56-1078-478d-8261-0b366744f4e5
Demo code and traceback
from rich.markdown import Markdown
from textual import on
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.widgets import Select
class MyApp(App):
def compose(self) -> ComposeResult:
with Horizontal():
yield Select[Markdown](
(
("h1", Markdown("# This is header 1")),
("h2", Markdown("## This is header 2")),
("h3", Markdown("### This is header 3")),
),
prompt="Choose a header ...",
id="id1",
)
yield Select[Markdown](
(
("bold", Markdown("**This is bold**")),
("italic", Markdown("*This is italics*")),
),
prompt="Choose a format ...",
id="id2",
)
@on(Select.Changed, "#id1")
def handle_id1_change(self, event: Select.Changed):
# This will not work
err = self.query_one("#id2", Select[Markdown])
reveal_type(err) # Select[Markdown] - correct, but crashes at runtime
╭──────────────────────────── Traceback (most recent call last) ─────────────────────────────╮
│ /Users/tushar.chandra/work/apis/examples/demo_subscript.py:32 in handle_id1_change │
│ │
│ 29 │ @on(Select.Changed, "#id1") │
│ 30 │ def handle_id1_change(self, event: Select.Changed): │
│ 31 │ │ # This will not work │
│ ❱ 32 │ │ err = self.query_one("#id2", Select[Markdown]) │
│ 33 │ │ reveal_type(err) # Select[Markdown] - correct, but fails at runtime │
│ 34 │
│ │
│ ╭─────────────────────── locals ───────────────────────╮ │
│ │ event = Changed() │ │
│ │ self = MyApp(title='MyApp', classes={'-dark-mode'}) │ │
│ ╰──────────────────────────────────────────────────────╯ │
│ │
│ /Users/tushar.chandra/.pyenv/versions/3.11.3/lib/python3.11/typing.py:1292 in │
│ __instancecheck__ │
│ │
│ 1289 │ │ │ setattr(self.__origin__, attr, val) │
│ 1290 │ │
│ 1291 │ def __instancecheck__(self, obj): │
│ ❱ 1292 │ │ return self.__subclasscheck__(type(obj)) │
│ 1293 │ │
│ 1294 │ def __subclasscheck__(self, cls): │
│ 1295 │ │ raise TypeError("Subscripted generics cannot be used with" │
│ │
│ ╭─────────────────────────── locals ────────────────────────────╮ │
│ │ obj = Select(id='id2') │ │
│ │ self = textual.widgets._select.Select[rich.markdown.Markdown] │ │
│ ╰───────────────────────────────────────────────────────────────╯ │
│ │
│ /Users/tushar.chandra/.pyenv/versions/3.11.3/lib/python3.11/typing.py:1295 in │
│ __subclasscheck__ │
│ │
│ 1292 │ │ return self.__subclasscheck__(type(obj)) │
│ 1293 │ │
│ 1294 │ def __subclasscheck__(self, cls): │
│ ❱ 1295 │ │ raise TypeError("Subscripted generics cannot be used with" │
│ 1296 │ │ │ │ │ │ " class and instance checks") │
│ 1297 │ │
│ 1298 │ def __dir__(self): │
│ │
│ ╭─────────────────────────── locals ────────────────────────────╮ │
│ │ cls = <class 'textual.widgets._select.Select'> │ │
│ │ self = textual.widgets._select.Select[rich.markdown.Markdown] │ │
│ ╰───────────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: Subscripted generics cannot be used with class and instance checks
❯ textual diagnose
## Versions
Name | Value |
---|---|
Textual | 0.28.1 |
Rich | 13.4.2 |
Python
Name | Value |
---|---|
Version | 3.11.3 |
Implementation | CPython |
Compiler | Clang 13.1.6 (clang-1316.0.21.2.5) |
Executable | /Users/tushar.chandra/Library/Caches/pypoetry/virtualenvs/tempus-apis-m3bbOr4E-py3.11/bin/python |
Operating System
Name | Value |
---|---|
System | Darwin |
Release | 21.5.0 |
Version | Darwin Kernel Version 21.5.0: Tue Apr 26 21:08:22 PDT 2022; root:xnu-8020.121.3~4/RELEASE_X86_64 |
Terminal
Name | Value |
---|---|
Terminal Application | vscode (1.79.2) |
TERM | xterm-256color |
COLORTERM | truecolor |
FORCE_COLOR | Not set |
NO_COLOR | Not set |
Rich Console options
Name | Value |
---|---|
size | width=94, height=36 |
legacy_windows | False |
min_width | 1 |
max_width | 94 |
is_terminal | True |
encoding | utf-8 |
max_height | 36 |
justify | None |
overflow | None |
no_wrap | False |
highlight | None |
markup | None |
height | None |
I don't think there is a practical solution I'm afraid.
There probably isn't any point in fully typing the generics in compose
as the generic part would be forgotten about after it is yield.
Feel free to add a line to the docstring to explain that generics can't be used by query
In Pylance / Pyright / VSCode this manifests as e.g.:
Type of "..." is partially unknown
Type of "..." is "DataTable"Pylance[reportUnknownVariableType]
https://github.com/microsoft/pyright/blob/main/docs/configuration.md#reportUnknownVariableType)