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

Mounting duplicate widget IDs can result in a NoScreen error #2863

Closed
davep opened this issue Jun 29, 2023 · 6 comments
Closed

Mounting duplicate widget IDs can result in a NoScreen error #2863

davep opened this issue Jun 29, 2023 · 6 comments
Labels
bug Something isn't working Can not reproduce The issue couldn't be reproduced Task

Comments

@davep
Copy link
Contributor

davep commented Jun 29, 2023

I'm writing this one up near the end of the day, at the start of a holiday, just so it's written down and I/we don't forget. Annoyingly I can't recreate the problem in isolation, only in the code I'm working on at the moment.

It is possible to get into a situation where an attempt to mount a widget with an ID that already exists amongst its would-be siblings results in a NoScreen error being thrown from somewhere deep in the compositor code rather than the DuplicateIds, which is what you'd expect.

The error I was seeing is follows at the end of here.

Quick testing suggests that NodeList._append and NodeList._insert are the issue here. Making a change akin to this:

@@ -74,12 +74,12 @@ class NodeList(Sequence["Widget"]):
             widget: A widget.
         """
         if widget not in self._nodes_set:
-            self._nodes.append(widget)
-            self._nodes_set.add(widget)
             widget_id = widget.id
             if widget_id is not None:
                 self._ensure_unique_id(widget_id)
                 self._nodes_by_id[widget_id] = widget
+            self._nodes.append(widget)
+            self._nodes_set.add(widget)
             self._updates += 1
 
     def _insert(self, index: int, widget: Widget) -> None:

seems to address the issue.

If I can work out an isolated reproduction of this I'll add it here, but the above change does seem to make some sense I think.

...
│                                                                                                                                                                       │
│ /Users/davep/develop/python/textual/src/textual/dom.py:477 in screen                                                                                                  │
│                                                                                                                                                                       │
│    474 │   │   while node is not None and not isinstance(node, Screen):                                                                                               │
│    475 │   │   │   node = node._parent                                                                                                                                │
│    476 │   │   if not isinstance(node, Screen):                                                                                                                       │
│ ❱  477 │   │   │   raise NoScreen("node has no screen")                                                                                                               │
│    478 │   │   return node                                                                                                                                            │
│    479 │                                                                                                                                                              │
│    480 │   @property                                                                                                                                                  │
│                                                                                                                                                                       │
│ ╭───────────────────────────────────── locals ──────────────────────────────────────╮                                                                                 │
│ │   node = None                                                                     │                                                                                 │
│ │ Screen = <class 'textual.screen.Screen'>                                          │                                                                                 │
│ │   self = RightAlignToast(id='textual-toast-91b49506-d61a-4698-ac8b-5bc491fdd5c9') │                                                                                 │
│ ╰───────────────────────────────────────────────────────────────────────────────────╯                                                                                 │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
NoScreen: node has no screen
@davep davep added bug Something isn't working Task labels Jun 29, 2023
davep added a commit to davep/textual that referenced this issue Jun 30, 2023
Yes, this does go against all things Pythonic, but in this case it's likely
less costly to do the check first; moreover it works around the problem I
ran in to: Textualize#2863
willmcgugan added a commit that referenced this issue Jul 17, 2023
* Add a notification class and a class to hold notifications

This provides the core classes for holding information on a single
notification, and then on top of that a class for managing a collection of
notifications.

* WiP: End of day/week commit to pick up post-holiday

* Ask permission rather than forgiveness

Yes, this does go against all things Pythonic, but in this case it's likely
less costly to do the check first; moreover it works around the problem I
ran in to: #2863

* Move the handling of "I've seen this" into the toast rack

This way the interface becomes "here's a bunch of notifications, you go work
this out".

* Add a notify method to all widgets

* The removal time for a toast should be the time left

When it was per-screen, it made sense that it was the timeout; now that
we're carrying them over between screens we're going to make sure they're
only around for as long as they need to be.

* Carry notifications between screens

* Remove the test code

* Drop the borders from the toasts

Except for the title, keep that.

* Provide access to the notification timeout

* Remove the title panel from a Toast if the title is empty

* Make the Toast CSS classes "private"

Prefix with a - to reduce the chance of a clash with userspace.

* Refresh a docstring

* Stop widget leakage

The Toasts were removing themselves, but they're wrapped inside a helper
container that keeps them aligned right. So the problem was that the
alignment containers were leaking. This ensures that when a Toast goes away
it takes its parent with it.

* Make the alignment container hidden

This doesn't really make any difference, but it feels like it makes sense to
hide it if there's nothing to show -- it's purely for alignment.

* 🚚 Rename the toast container

This is about getting the toasts to align correctly (even when you do align
things, they don't really align as expected due to the way that a container
aligns the bounding box of all if its children, not the individual
children). However, I had this named after where it aligned them to; someone
using the system may wish to change that, so let's make the name more
generic.

* Improve ToastRack._toast_id

Add a docstring, and also change the format of the identity somewhat so that
it's even "more internal".

* Add some initial low-level notification testing

* Add initial testing of notifications within an application

* Add tests for notifying from the 3 main levels within the DOM

* Add a toast example to the docs and a snapshot test

This might not be the final form, but it'll do for the moment. I want to get
the snapshot test in place at least.

* Add a snapshot test for notifications persisting between screens

* Add some documentation for a Toast

This isn't going into the index, just yet. This is *technically* an internal
widget so I'm not sure how and where it makes sense to document it; if at
all. But let's get some documentation in here anyway.

* Flesh out the docstrings for the notify methods

* Add a missing docstring to the Notifications __init__ method

* Add snapshot tests for persisting notifications through mode switches

* Remove unused import

Looks like eglot/pyright tried to be "helpful" at some point and I didn't
notice.

* Correct the Toast severity level classes in the docs

Originally they weren't in the "internal" namespace, then I decided that
they should be so there's less chance of a clash with dev-space code; but I
forgot to reflect this in the docs.

This fixes that.

* Make the removal of notifications/toasts a two way thing

The addition of the ability to dismiss a toast by clicking on it had a flaw:
the notification->toast code had been written with things being one way. The
expiration of notifications happened in the notification handler, and the
expiration of Toasts was done in the Toast system, on purpose (notifications
might end up being routed via elsewhere so this needs to be done).

But... this meant that hand-removed Toasts kept coming back from the dead
when a new notification was raised iff the hand-removed ones hadn't yet
expired.

So here I add the ability the remove a notification from the notification
collection.

* Remove an unhelpful comment

Sort of a hangover from what was initially looking like it was going to be a
longer body of code. It doesn't really need explaining any more.

* Add in support to the notification collection

* Change the toast rack adder to be a general "show" method

This turns the method into one that further aids making the connection
between the notifications and the toasts two way. Now it makes sense that if
there are toasts for notifications that no longer exist, they also get
removed.

This makes it easier to add all sorts of clear options later on.

* Add a method to clear notifications

* Add an App method for clearing all existing notifications

* Add a missing docstring to _refresh_notifications

* Return the notification from the notify methods

It can be seen as, and used as, a handle of sorts (see unnotify); so return
it so people can use it.

* Add some more notifications unit testing

* Add some more app-level notification unit testing

* Style tweaks

* docs

* added notifications

* snapshots

---------

Co-authored-by: Will McGugan <[email protected]>
@davep
Copy link
Contributor Author

davep commented Jul 18, 2023

I've just spent about 40 minutes trying to isolate this error (something not made any easier by not actually having the non-isolated code easily to hand as that was an earlier incarnation of the notification code) and have utterly failed. I'm not overly keen on fixing something for which a test can't be constructed.

Should we blindly apply the suggested fix, knowing that it doesn't break existing functionality/tests? Or should we backlog this until such a time as we manage to recreate this error again?

@davep
Copy link
Contributor Author

davep commented Jul 19, 2023

We had wondered if it might be down to the fast removal and then mounting of widgets in the same location in the DOM, but attempting to recreate that didn't come to anything too.

Moving this to "blocked" for the moment; it would be best to fix this knowing what we're actually fixing. Meanwhile, if anyone gets the error reported at the start: let me know!

@maxking
Copy link

maxking commented Jul 20, 2023

I am seeing the same issue working on https://github.com/maxking/archive_reader.

I was relying on DuplicateIds to not having to check if I am adding dupe widgets, but I am seeing this error here, which results in the error:

https://github.com/maxking/archive_reader/blob/main/src/archive_reader/app.py#L370-L373

@willmcgugan
Copy link
Collaborator

@davep That may be the fix. Let's spend a couple of hours to see if we can recreate it.

@rodrigogiraoserrao rodrigogiraoserrao added the Can not reproduce The issue couldn't be reproduced label Nov 21, 2023
@rodrigogiraoserrao
Copy link
Contributor

This has been around for a while and we can't seem to reproduce it regardless of how hard we try.
We agreed to close this for now; maybe it's been fixed as a consequence of other changes we made in the past 5 months.

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
bug Something isn't working Can not reproduce The issue couldn't be reproduced Task
Projects
None yet
Development

No branches or pull requests

4 participants