textual icon indicating copy to clipboard operation
textual copied to clipboard

Subscripted generics cannot be used in query selectors

Open tuchandra opened this issue 1 year ago • 2 comments

(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's compose 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

tuchandra avatar Jun 30 '23 16:06 tuchandra

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

willmcgugan avatar Jul 06 '23 13:07 willmcgugan

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)

davetapley avatar Dec 04 '23 17:12 davetapley