Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugs related to multi-screen approach #1846

Closed
mzebrak opened this issue Feb 20, 2023 · 4 comments · Fixed by #1873
Closed

Bugs related to multi-screen approach #1846

mzebrak opened this issue Feb 20, 2023 · 4 comments · Fixed by #1873

Comments

@mzebrak
Copy link

mzebrak commented Feb 20, 2023

Version: 0.11.0

Consider a following code:

from textual.app import App
from textual.binding import Binding
from textual.screen import Screen
from textual.widget import Widget
from textual.widgets import Footer, Header, Placeholder


class SomeWidget(Widget):
    def compose(self):
        yield Placeholder('SomeWidget')


class BaseScreen(Screen):

    def compose(self):
        # looks like heres also a problem - adding this line breaks the app `NoMatches: No nodes match <DOMQuery Header() filter='HeaderTitle'>`
        # yield Header()

        yield SomeWidget()
        yield Footer()


class FirstScreen(BaseScreen):
    BINDINGS = [
        Binding('n', 'switch_screen("second")', 'Second screen - via name'),
        Binding('m', 'switch', 'Second screen - via object'),
    ]

    def action_switch(self):
        self.app.switch_screen(SecondScreen())


class SecondScreen(BaseScreen):
    BINDINGS = [
        Binding('n', 'switch_screen("first")', 'First screen - via name'),
        Binding('m', 'switch', 'First screen - via objecct'),

    ]

    def action_switch(self):
        self.app.switch_screen(FirstScreen())


class MyApp(App):
    SCREENS = {
        'first': FirstScreen,
        'second': SecondScreen,
    }

    BINDINGS = [
        Binding('q', 'query', 'Query for SomeWidget'),
    ]

    def on_mount(self):
        # with 'first', we got 2 of SomeWidget's after first keypress (m). When there is `First()`, we got a 1 SomeWidget
        self.push_screen(FirstScreen())

    def action_query(self):
        self.log(f"Current stack: {self.screen_stack}")

        # Should be always equal to 1, but sometimes it's 2 or even 3 (while switching via both 'n' and 'm' keys)
        self.log(f"Number of SomeWidget's: {len(self.query(SomeWidget))}")


if __name__ == '__main__':
    MyApp().run()

The example provided above shows some problems. Try launching it and switching views via "n" and "m" keys.
I think it's a descriptive example so there's not much to add, but it seems that there are 2 bugs in this piece of code - one is a problem with displaying the header, because after uncommenting it throws an error, and second problem occurs when switching screens.

@github-actions
Copy link

Thank you for your issue. Give us a little time to review it.

PS. You might want to check the FAQ if you haven't done so already.

This is an automated reply, generated by FAQtory

@willmcgugan
Copy link
Collaborator

I can't reproduce any issue with that code. The error you mention in a comment suggests you are running an older version of Textual. Can you please double check you are running 0.11.0. Running textual diagnose will help with that.

@mzebrak
Copy link
Author

mzebrak commented Feb 23, 2023

I can't reproduce any issue with that code. The error you mention in a comment suggests you are running an older version of Textual. Can you please double check you are running 0.11.0. Running textual diagnose will help with that.

That's interesting because:

(screens-py3.10) PS D:\PycharmProjects\screens> textual diagnose
# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.11.0 |
| Rich    | 13.3.1 |

## Python

| Name           | Value                                                                                             |
|----------------|---------------------------------------------------------------------------------------------------|
| Version        | 3.10.7                                                                                            |
| Implementation | CPython                                                                                           |
| Compiler       | MSC v.1933 64 bit (AMD64)                                                                         |
| Executable     | C:\Users\...\AppData\Local\pypoetry\Cache\virtualenvs\screens-XJncIhWR-py3.10\Scripts\python.exe |

## Operating System

| Name    | Value      |
|---------|------------|
| System  | Windows    |
| Release | 10         |
| Version | 10.0.19044 |

## Terminal

| Name                 | Value            |
|----------------------|------------------|
| Terminal Application | Windows Terminal |
| TERM                 | *Not set*        |
| COLORTERM            | *Not set*        |
| FORCE_COLOR          | *Not set*        |
| NO_COLOR             | *Not set*        |

## Rich Console options

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

Problem with switching screens:

  1. When I run textual console and then textual run --dev <script_path>:
  • press 'N', press 'Q' -> "Number of SomeWidget's: 1", "Current stack: [Screen(id='_default'), SecondScreen()]"

  • press 'N', press 'Q' -> "Number of SomeWidget's: 2", "Current stack: [Screen(id='_default'), FirstScreen()]"

  • press 'N', press 'Q' -> "Number of SomeWidget's: 2", "Current stack: [Screen(id='_default'), SecondScreen()]"

  • press 'M', press 'Q' -> "Number of SomeWidget's: 3", "Current stack: [Screen(id='_default'), FirstScreen()]"

  • press 'M', press 'Q' -> "Number of SomeWidget's: 3", "Current stack: [Screen(id='_default'), SecondScreen()]"

  • press 'N', press 'Q' -> "Number of SomeWidget's: 2", "Current stack: [Screen(id='_default'), FirstScreen()]"

    And of course amount of SomeWidget should be always equal to 1. So there is a problem clearly visible when switching screens via switch_screen("first") and switch_screen("second")

  1. When I run textual console and then textual run --dev <script_path>:
  • press 'M', press 'Q' -> "Number of SomeWidget's: 1", "Current stack: [Screen(id='_default'), SecondScreen()]"

  • press 'M', press 'Q' -> "Number of SomeWidget's: 1", "Current stack: [Screen(id='_default'), FirstScreen()]"

  • press 'M', press 'Q' -> "Number of SomeWidget's: 1", "Current stack: [Screen(id='_default'), SecondScreen()]"

    No problem when switching screens via self.app.switch_screen(FirstScreen()) and self.app.switch_screen(SecondScreen())

I don't think it is a windows platform-specific bug since as I correctly remember - saw it for the first time under the Ubuntu:22.04 environment.

Problem with Header throwing an exception:

  1. Uncomment the yield Header() in BaseScreen's compose method.
  2. textual run --dev <script_path>
  3. Press "N" or "M", it doesn't matter, exception is raised in both ways.

Traceback:

╭───────────────────────────────────────── Traceback (most recent call last) ──────────────────────────────────────────╮
│ C:\Users\...\AppData\Local\pypoetry\Cache\virtualenvs\screens-XJncIhWR-py3.10\lib\site-packages\textual\widgets\_he │
│ ader.py:136 in on_mount                                                                                              │
│                                                                                                                      │
│   133 │   │   def set_sub_title(sub_title: str) -> None:                                                             │
│   134 │   │   │   self.query_one(HeaderTitle).sub_text = sub_title                                                   │
│   135 │   │                                                                                                          │
│ ❱ 136 │   │   self.watch(self.app, "title", set_title)                                                               │
│   137 │   │   self.watch(self.app, "sub_title", set_sub_title)                                                       │
│   138                                                                                                                │
│                                                                                                                      │
│ ╭──────────────────────────────────────── locals ─────────────────────────────────────────╮                          │
│ │          self = Header()                                                                │                          │
│ │ set_sub_title = <function Header.on_mount.<locals>.set_sub_title at 0x0000023B81A181F0> │                          │
│ │     set_title = <function Header.on_mount.<locals>.set_title at 0x0000023B818FFC70>     │                          │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────╯                          │
│                                                                                                                      │
│ C:\Users\...\AppData\Local\pypoetry\Cache\virtualenvs\screens-XJncIhWR-py3.10\lib\site-packages\textual\dom.py:669  │
│ in watch                                                                                                             │
│                                                                                                                      │
│   666 │   │   │   callback: A callback to run when attribute changes.                                                │
│   667 │   │   │   init: Check watchers on first call.                                                                │
│   668 │   │   """                                                                                                    │
│ ❱ 669 │   │   _watch(self, obj, attribute_name, callback, init=init)                                                 │
│   670 │                                                                                                              │
│   671 │   def get_pseudo_classes(self) -> Iterable[str]:                                                             │
│   672 │   │   """Get any pseudo classes applicable to this Node, e.g. hover, focus.                                  │
│                                                                                                                      │
│ ╭─────────────────────────────────────── locals ───────────────────────────────────────╮                             │
│ │ attribute_name = 'title'                                                             │                             │
│ │       callback = <function Header.on_mount.<locals>.set_title at 0x0000023B818FFC70> │                             │
│ │           init = True                                                                │                             │
│ │            obj = MyApp(title='MyApp', classes={'-dark-mode'})                        │                             │
│ │           self = Header()                                                            │                             │
│ ╰──────────────────────────────────────────────────────────────────────────────────────╯                             │
│                                                                                                                      │
│ C:\Users\...\AppData\Local\pypoetry\Cache\virtualenvs\screens-XJncIhWR-py3.10\lib\site-packages\textual\reactive.py │
│ :354 in _watch                                                                                                       │
│                                                                                                                      │
│   351watcher_list.append((node, callback))                                                                      │
│   352if init:                                                                                                   │
│   353 │   │   current_value = getattr(obj, attribute_name, None)                                                     │
│ ❱ 354 │   │   Reactive._check_watchers(obj, attribute_name, current_value)                                           │
│   355                                                                                                                │
│                                                                                                                      │
│ ╭───────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────╮ │
│ │ attribute_name = 'title'                                                                                         │ │
│ │       callback = <function Header.on_mount.<locals>.set_title at 0x0000023B818FFC70>                             │ │
│ │  current_value = 'MyApp'                                                                                         │ │
│ │           init = True                                                                                            │ │
│ │           node = Header()                                                                                        │ │
│ │            obj = MyApp(title='MyApp', classes={'-dark-mode'})                                                    │ │
│ │   watcher_list = [                                                                                               │ │
│ │                  │   (Header(), <function Header.on_mount.<locals>.set_title at 0x0000023B818FEC20>),            │ │
│ │                  │   (Header(), <function Header.on_mount.<locals>.set_title at 0x0000023B818FFC70>)             │ │
│ │                  ]                                                                                               │ │
│ │       watchers = {                                                                                               │ │
│ │                  │   'title': [                                                                                  │ │
│ │                  │   │   (Header(), <function Header.on_mount.<locals>.set_title at 0x0000023B818FEC20>),        │ │
│ │                  │   │   (Header(), <function Header.on_mount.<locals>.set_title at 0x0000023B818FFC70>)         │ │
│ │                  │   ],                                                                                          │ │
│ │                  │   'sub_title': [                                                                              │ │
│ │                  │   │   (                                                                                       │ │
│ │                  │   │   │   Header(),                                                                           │ │
│ │                  │   │   │   <function Header.on_mount.<locals>.set_sub_title at 0x0000023B818FE4D0>             │ │
│ │                  │   │   )                                                                                       │ │
│ │                  │   ]                                                                                           │ │
│ │                  }                                                                                               │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                                      │
│ C:\Users\...\AppData\Local\pypoetry\Cache\virtualenvs\screens-XJncIhWR-py3.10\lib\site-packages\textual\widgets\_he │
│ ader.py:131 in set_title                                                                                             │
│                                                                                                                      │
│   128 │                                                                                        ╭───── locals ─────╮  │
│   129def on_mount(self) -> None:                                                          │  self = Header() │  │
│   130 │   │   def set_title(title: str) -> None:                                               │ title = 'MyApp'  │  │
│ ❱ 131 │   │   │   self.query_one(HeaderTitle).text = title                                     ╰──────────────────╯  │
│   132 │   │                                                                                                          │
│   133 │   │   def set_sub_title(sub_title: str) -> None:                                                             │
│   134 │   │   │   self.query_one(HeaderTitle).sub_text = sub_title                                                   │
│                                                                                                                      │
│ C:\Users\...\AppData\Local\pypoetry\Cache\virtualenvs\screens-XJncIhWR-py3.10\lib\site-packages\textual\dom.py:839  │
│ in query_one                                                                                                         │
│                                                                                                                      │
│   836 │   │   │   query_selector = selector.__name__                                                                 │
│   837 │   │   query: DOMQuery[Widget] = DOMQuery(self, filter=query_selector)                                        │
│   838 │   │                                                                                                          │
│ ❱ 839 │   │   return query.only_one() if expect_type is None else query.only_one(expect_type)                        │
│   840 │                                                                                                              │
│   841def set_styles(self, css: str | None = None, **update_styles) -> None:                                     │
│   842 │   │   """Set custom styles on this object."""                                                                │
│                                                                                                                      │
│ ╭──────────────────────────── locals ────────────────────────────╮                                                   │
│ │       DOMQuery = <class 'textual.css.query.DOMQuery'>          │                                                   │
│ │    expect_type = None                                          │                                                   │
│ │          query = <DOMQuery Header() filter='HeaderTitle'>      │                                                   │
│ │ query_selector = 'HeaderTitle'                                 │                                                   │
│ │       selector = <class 'textual.widgets._header.HeaderTitle'> │                                                   │
│ │           self = Header()                                      │                                                   │
│ ╰────────────────────────────────────────────────────────────────╯                                                   │
│                                                                                                                      │
│ C:\Users\...\AppData\Local\pypoetry\Cache\virtualenvs\screens-XJncIhWR-py3.10\lib\site-packages\textual\css\query.p │
│ y:243 in only_one                                                                                                    │
│                                                                                                                      │
│   240 │   │   """                                                                                                    │
│   241 │   │   # Call on first to get the first item. Here we'll use all of the                                       │
│   242 │   │   # testing and checking it provides.                                                                    │
│ ❱ 243 │   │   the_one = self.first(expect_type) if expect_type is not None else self.first()                         │
│   244 │   │   try:                                                                                                   │
│   245 │   │   │   # Now see if we can access a subsequent item in the nodes. There                                   │
│   246 │   │   │   # should *not* be anything there, so we *should* get an                                            │
│                                                                                                                      │
│ ╭──────────────────────── locals ────────────────────────╮                                                           │
│ │ expect_type = None                                     │                                                           │
│ │        self = <DOMQuery Header() filter='HeaderTitle'> │                                                           │
│ ╰────────────────────────────────────────────────────────╯                                                           │
│                                                                                                                      │
│ C:\Users\...\AppData\Local\pypoetry\Cache\virtualenvs\screens-XJncIhWR-py3.10\lib\site-packages\textual\css\query.p │
│ y:214 in first                                                                                                       │
│                                                                                                                      │
│   211 │   │   │   │   │   )                                                                                          │
│   212 │   │   │   return first                                                                                       │
│   213 │   │   else:                                                                                                  │
│ ❱ 214 │   │   │   raise NoMatches(f"No nodes match {self!r}")                                                        │
│   215 │                                                                                                              │
│   216 │   @overload                                                                                                  │
│   217def only_one(self) -> Widget:                                                                              │
│                                                                                                                      │
│ ╭──────────────────────── locals ────────────────────────╮                                                           │
│ │ expect_type = None                                     │                                                           │
│ │        self = <DOMQuery Header() filter='HeaderTitle'> │                                                           │
│ ╰────────────────────────────────────────────────────────╯                                                           │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
NoMatches: No nodes match <DOMQuery Header() filter='HeaderTitle'>

@github-actions
Copy link

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants