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

self.app.uninstall_screen() does not delete child nodes #1580

Closed
toiletsandpaper opened this issue Jan 17, 2023 · 5 comments
Closed

self.app.uninstall_screen() does not delete child nodes #1580

toiletsandpaper opened this issue Jan 17, 2023 · 5 comments
Labels

Comments

@toiletsandpaper
Copy link

toiletsandpaper commented Jan 17, 2023

Please give a brief but clear explanation of the issue.

So, I have a Back button, that do:

elif event.button.id == 'button-back':
    self.app.pop_screen()
    self.app.uninstall_screen('csv-file-editor')

I need it to open the file, do something with this file, and go back. But when I do install_screen('csv-file-editor') -> install_screen('csv-file-scren') -> self.app.query_one('#csv-file-editor') - I've get an error that found to many DOM nodes.
This behavior can be simply fixed by self.screen.query_one() instead of self.app.query_one(), but this is really looks like a bug, because uninstall_screen() should also uninstall all sub-widgets that was in compose

What Operating System are you running on?

Windows 10 21H2

Feel free to add screenshots and/or videos. These can be very helpful!

class CsvFileEditorScreen(Screen):
    ...
    def compose(self) -> ComposeResult:
        ...
        yield Horizontal(
            CsvDataTable(show_header=True, zebra_stripes=True, id='csv-data-table'),
            classes='preview-container'
        )
        ...
    
    def on_Button_pressed(self, event: Button.Pressed) -> None:
        ...
        elif event.button.id == 'button-back':
            self.app.pop_screen()
            self.app.uninstall_screen('csv-file-editor')
            raise Exception(self.app.screen_stack, self.app.query('#csv-data-table').nodes)
            ...

Outputs:

╭───────────────────────────────────────── Traceback (most recent call last) ──────────────────────────────────────────╮
│ C:\#######:430 in on_button_pressed                                          │
│                                                                                                                      │
│   427 │   │   │   #csv_dt = self.app.get_widget_by_id('csv-data-table')                                              │428 │   │   │   self.app.pop_screen()                                                                              │
│   429 │   │   │   self.app.uninstall_screen('csv-file-editor')                                                       │
│ ❱ 430 │   │   │   raise Exception(self.app.screen_stack, self.app.query('#csv-data-table').nod                       │
│   431 │   │   # if pressed button is in form of `X-[plus|minus]`,                                                    │432 │   │   # then `self.input_X.index_count` ++ or --                                                             │433 │   │   # and if it's empty string '', then `self.input_X.index_count = 0`                                     │
│                                                                                                                      │
│ ╭────────────────────────────── locals ──────────────────────────────╮                                               │
│ │ event = Pressed(                                                   │                                               │
│ │         │   ButtonBack(                                            │                                               │
│ │         │   │   id='button-back',                                  │                                               │
│ │         │   │   classes={'-active', 'button-back', '-error'},      │                                               │
│ │         │   │   pseudo_classes={                                   │                                               │
│ │         │   │   │   'hover',                                       │                                               │
│ │         │   │   │   'focus-within',                                │                                               │
│ │         │   │   │   'focus'                                        │                                               │
│ │         │   │   },                                                 │                                               │
│ │         │   │   variant='error'                                    │                                               │
│ │         │   )                                                      │                                               │
│ │         )                                                          │                                               │
│ │  self = CsvFileEditorScreen(pseudo_classes={'focus-within'})       │                                               │
│ ╰────────────────────────────────────────────────────────────────────╯                                               │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Exception: ([Screen(id='_default'), FilePickerScreen(pseudo_classes={'focus-within'}),
FilePreviewScreen(pseudo_classes={'focus-within'})], [CsvDataTable(id='csv-data-table')])

And as you can see - screen CsvFileEditorScreen was uninstalled, but widget, that was installed in compose section was not uninstalled, which creates useless objects in memory

P.S.

IDK why, but Traceback output cutted es) from 430 │ │ │ raise Exception(self.app.screen_stack, self.app.query('#csv-data-table').nodes), this is not my modification - it's a real output. Probably one more bug to solve? :)

@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

@davep
Copy link
Contributor

davep commented Jan 20, 2023

I was having a little bit of trouble following quite how your app was holding together, so I've attempted to write a standalone example of what I think you're seeing. Note that this doesn't push or pop screens, it only installs or uninstalls them.

from textual.app        import App, ComposeResult
from textual.containers import Vertical, Horizontal
from textual.widgets    import Header, Footer, Static, Label
from textual.screen     import Screen
from textual.binding    import Binding
from rich.pretty        import Pretty

class ChildScreen( Screen ):

    def compose( self ) -> ComposeResult:
        """Compose the child widgets."""
        yield Label( "Press space to make me go away", id="only-on-child" )

class Display( Static, can_focus=True ):
    pass

class DOMView( Display ):
    pass

class ScreenView( Display ):
    pass

class QueryResult( Display ):
    pass

class ScreenAndDomApp( App[ None ] ):

    CSS = """
    .box {
        border: round yellow;
    }
    .box:focus-within {
        border: double yellow;
    }

    Horizontal > Vertical {
        width: 1fr;
    }
    """

    BINDINGS = [
        Binding( "i", "install", "Install Screen" ),
        Binding( "u", "uninstall", "Uninstall Screen" )
    ]

    def __init__( self ) -> None:
        super().__init__()
        self.screen_id                 = 0
        self.screen_names: list[ str ] = []

    def compose( self ) -> ComposeResult:
        yield Header()
        yield Vertical(
            Vertical( DOMView(), classes="box" ),
            Horizontal(
                Vertical( ScreenView(), classes="box" ),
                Vertical( QueryResult(), classes="box" )
            )
        )
        yield Footer()

    @property
    def installed_screens( self ):
        return [ ( k, v ) for k, v in self._installed_screens.items() ]

    def refresh_panels( self ) -> None:
        self.query_one( DOMView ).update( self.tree )
        self.query_one( ScreenView ).update( Pretty( self.installed_screens ) )
        self.query_one( QueryResult ).update( Pretty( list( self.query( "#only-on-child" ) ) ) )

    def on_mount( self ) -> None:
        self.refresh_panels()

    def action_install( self ) -> None:
        self.screen_names.append( f"child-{ self.screen_id}" )
        self.screen_id += 1
        self.install_screen( ChildScreen(), self.screen_names[ -1 ] )
        self.refresh_panels()

    def action_uninstall( self ) -> None:
        if self.screen_names:
            self.uninstall_screen( self.screen_names.pop() )
            self.refresh_panels()

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

I am seeing behaviour I wouldn't have expected. Here's what happens when I install 5 screens then uninstall them again:

Screenshot 2023-01-20 at 17 15 13

The top panel can be ignored for the moment, it's just a dump of the whole DOM as a tree. Left bottom is the list of installed screens; so it's showing that I no longer have screens installed. Bottom right is performing a query on a widget that is only on the screen I'm installing, and which has subsequently been removed as many times as it was installed.

I don't have an answer for this at the moment, but I just wanted to get a good stand-alone example of the problem created and recorded here.

@davep davep added the Task label Jan 20, 2023
davep added a commit to davep/textual-sandbox that referenced this issue Jan 20, 2023
@willmcgugan
Copy link
Collaborator

I think there may be some confusion about what it means to install and uninstall a screen.

Installing a screen gives it a name and prevents Textual from closing it when you pop it off the stack. You generally only want to install screens that stick around for a while (typically the lifetime of your app). See named screens in the docs.

If you want a screen to act like a modal dialog you can use push_screen(MyScreen()) followed by pop_screen.

@willmcgugan
Copy link
Collaborator

Docs have been expanded in #1778

@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
Projects
None yet
Development

No branches or pull requests

3 participants