-
Notifications
You must be signed in to change notification settings - Fork 815
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
MountError: Can't mount widget(s) before <widget> is mounted
with widgets=()
during dynamic refresh
#4570
Comments
Yeah, many of my apps are failing due to this error. Here's a MVP of my issue: from textual.app import App
from textual.widgets import Button
from textual.containers import VerticalScroll, Vertical
class TestApp(App):
def __init__(self):
super().__init__()
self.container = VerticalScroll()
def compose(self):
yield self.container
def on_mount(self):
self.dark = False
self.chk_mount()
def chk_mount(self):
foo = Vertical()
foo.mount(Button('Click me'))
self.container.mount(foo)
if __name__ == "__main__":
app = TestApp()
app.run() In my real usecase, the widgets in the |
@learnbyexample Mounting is for putting a widget into the DOM, not for nesting not-yet-mounted widgets inside each other. To nest widgets you just pass them as children, and then mount the tree of widgets from the root: def chk_mount(self):
self.container.mount(Vertical(Button('Click me'))) |
In the MRE in the original post, it seems like I suspect If there are new children, then the new check that gets triggered doesn't make sense (since we're not actually trying to mount any children - Label has none). |
@darrenburns I'll try it out tomorrow and let you know. The Edit: Okay, I think you mean |
@learnbyexample You can nest like this to build up the tree of widgets however you like: stuff = Vertical(
Button("top button"),
Button("middle button"),
Button("bottom button"),
Horizontal(
Label("foo"),
Label("bar"),
Vertical(
Label("one"),
Label("two"),
Label("three"),
),
),
) Then just mount like this: |
@mzebrak If you call The solution would be to make the method async and await mount and remove. This will also make batch_update work (in your example it had no effect). If you do that it should work, however it still leaves a traceback on exit, which I am looking in to now. |
@darrenburns @willmcgugan Thanks, I got it working for my use case. I had to collect the dynamically determined widgets in a list and then pass it to a container (along with other containers and widgets). |
Don't forget to star the repository! Follow @textualizeio for Textual updates. |
@willmcgugan Sorry for the late response, but I was away from the computer.
That sounds reasonable, but I didn't mean it flickers when I do: def sync(self):
with self.app.batch_update():
self.container.query(Label).remove()
new_items = self._create_container_content()
self.container.mount(*new_items) instead, I see a flicker when I do: def sync(self):
self.refresh(recompose=True) Another interesting thing is that we never observed an issue with
Unfortunately doing it your suggested way has another issue - everything freezes (which I've been talking initlaly in #3214), but I think the same thing happens in the MRE under. When spamming on the button in such an MRE makes the entire app to hang (hard to reproduce here again.. but in real app occurs very often): MRE with async remove and mount which freezes app on pop_screenfrom __future__ import annotations
from textual import on, work
from textual.app import App, ComposeResult
from textual.containers import VerticalScroll
from textual.reactive import var
from textual.screen import Screen
from textual.widgets import Button, Label
class ScreenToPop(Screen):
def compose(self) -> ComposeResult:
yield Button("press me to update and pop_screen", id="button")
@on(Button.Pressed)
def action_update(self):
self.app.watch_me = 0
self.app.pop_screen()
class MyScreen(Screen):
def __init__(self):
super().__init__()
self.labels_count = 0
self.container = VerticalScroll()
def compose(self) -> ComposeResult:
yield Button("press me to push screen again")
with self.container:
yield from self._create_container_content()
def on_mount(self):
self.watch(self.app, "watch_me", callback=self.sync)
async def sync(self):
with self.app.batch_update():
await self.container.query(Label).remove()
new_items = self._create_container_content()
await self.container.mount(*new_items)
@on(Button.Pressed)
def push_screen_again(self):
self.app.push_screen(ScreenToPop())
def _create_container_content(self) -> list[Label]:
labels_increment = 40
label_numbers = range(self.labels_count, self.labels_count + labels_increment)
self.labels_count += labels_increment
return [Label(f'label {i}') for i in label_numbers]
class MountErrorApp(App):
watch_me = var(0)
def on_mount(self):
self.set_interval(0.1, self.update_watch_me)
self.push_screen(MyScreen())
self.push_screen(ScreenToPop())
@work()
async def update_watch_me(self):
self.watch_me += 1
MountErrorApp().run() VideoScreencast.from.06-03-2024.08.25.36.AM.webmBut we already observed a workaround for this freeze which is: def on_mount(self):
def delegate_work() -> None:
self.run_worker(self.sync())
self.watch(self.app, "watch_me", callback=delegate_work) but still, doing it like this: MRE with freeze workaroundfrom __future__ import annotations
from textual import on, work
from textual.app import App, ComposeResult
from textual.containers import VerticalScroll
from textual.reactive import var
from textual.screen import Screen
from textual.widgets import Button, Label
class ScreenToPop(Screen):
def compose(self) -> ComposeResult:
yield Button("press me to update and pop_screen", id="button")
@on(Button.Pressed)
def action_update(self):
self.app.watch_me = 0
self.app.pop_screen()
class MyScreen(Screen):
def __init__(self):
super().__init__()
self.labels_count = 0
self.container = VerticalScroll()
def compose(self) -> ComposeResult:
yield Button("press me to push screen again")
with self.container:
yield from self._create_container_content()
def on_mount(self):
def delegate_work() -> None:
self.run_worker(self.sync())
self.watch(self.app, "watch_me", callback=delegate_work)
async def sync(self):
with self.app.batch_update():
await self.container.query(Label).remove()
new_items = self._create_container_content()
await self.container.mount(*new_items)
@on(Button.Pressed)
def push_screen_again(self):
self.app.push_screen(ScreenToPop())
def _create_container_content(self) -> list[Label]:
labels_increment = 40
label_numbers = range(self.labels_count, self.labels_count + labels_increment)
self.labels_count += labels_increment
return [Label(f'label {i}') for i in label_numbers]
class MountErrorApp(App):
watch_me = var(0)
def on_mount(self):
self.set_interval(0.1, self.update_watch_me)
self.push_screen(MyScreen())
self.push_screen(ScreenToPop())
@work()
async def update_watch_me(self):
self.watch_me += 1
MountErrorApp().run() throws an error: Traceback for freeze workaround - but still MountError╭─────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────────────────────────────────────────╮
│ /home/mzebrak/.pyenv/versions/clive/lib/python3.10/site-packages/textual/worker.py:365 in _run │
│ │
│ 362 │ │ self.state = WorkerState.RUNNING │
│ 363 │ │ app.log.worker(self) │
│ 364 │ │ try: │
│ ❱ 365 │ │ │ self._result = await self.run() │
│ 366 │ │ except asyncio.CancelledError as error: │
│ 367 │ │ │ self.state = WorkerState.CANCELLED │
│ 368 │ │ │ self._error = error │
│ │
│ ╭───────────────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────────────╮ │
│ │ app = MountErrorApp(title='MountErrorApp', classes={'-dark-mode'}) │ │
│ │ error = MountError("Can't mount widget(s) before VerticalScroll() is mounted") │ │
│ │ self = <Worker ERROR name='sync' description='<coroutine object MyScreen.sync at 0x7f653fe1a9d0>'> │ │
│ │ Traceback = <class 'rich.traceback.Traceback'> │ │
│ │ worker_failed = WorkerFailed('Worker raised exception: MountError("Can\'t mount widget(s) before VerticalScroll() is mounted")') │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /home/mzebrak/.pyenv/versions/clive/lib/python3.10/site-packages/textual/worker.py:349 in run │
│ │
│ 346 │ │ Returns: ╭────────────────────────────────────────────── locals ──────────────────────────────────────────────╮ │
│ 347 │ │ │ Return value of the work. │ self = <Worker ERROR name='sync' description='<coroutine object MyScreen.sync at 0x7f653fe1a9d0>'> │ │
│ 348 │ │ """ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ ❱ 349 │ │ return await ( │
│ 350 │ │ │ self._run_threaded() if self._thread_worker else self._run_async() │
│ 351 │ │ ) │
│ 352 │
│ │
│ /home/mzebrak/.pyenv/versions/clive/lib/python3.10/site-packages/textual/worker.py:336 in _run_async │
│ │
│ 333 │ │ ): ╭────────────────────────────────────────────── locals ──────────────────────────────────────────────╮ │
│ 334 │ │ │ return await self._work() │ self = <Worker ERROR name='sync' description='<coroutine object MyScreen.sync at 0x7f653fe1a9d0>'> │ │
│ 335 │ │ elif inspect.isawaitable(self._work): ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ ❱ 336 │ │ │ return await self._work │
│ 337 │ │ elif callable(self._work): │
│ 338 │ │ │ raise WorkerError("Request to run a non-async function as an async worker") │
│ 339 │ │ raise WorkerError("Unsupported attempt to run an async worker") │
│ │
│ /home/mzebrak/1workspace/clive/textual_mount_bug.py:41 in sync │
│ │
│ 38 │ │ with self.app.batch_update(): ╭──────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────╮ │
│ 39 │ │ │ await self.container.query(Label).remove() │ new_items = [Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label(), ... +30] │ │
│ 40 │ │ │ new_items = self._create_container_content() │ self = MyScreen() │ │
│ ❱ 41 │ │ │ await self.container.mount(*new_items) ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ 42 │ │
│ 43 │ @on(Button.Pressed) │
│ 44 │ def push_screen_again(self): │
│ │
│ /home/mzebrak/.pyenv/versions/clive/lib/python3.10/site-packages/textual/widget.py:938 in mount │
│ │
│ 935 │ │ │ provided a ``MountError`` will be raised. ╭─────────────────────────────────────────────────── locals ────────────────────────────────────────────────────╮ │
│ 936 │ │ """ │ after = None │ │
│ 937 │ │ if not self.is_attached: │ before = None │ │
│ ❱ 938 │ │ │ raise MountError(f"Can't mount widget(s) before {self!r} is mounted") │ self = VerticalScroll() │ │
│ 939 │ │ # Check for duplicate IDs in the incoming widgets │ widgets = (Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label(), Label(), ... +30) │ │
│ 940 │ │ ids_to_mount = [widget.id for widget in widgets if widget.id is not None] ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ 941 │ │ unique_ids = set(ids_to_mount) │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
MountError: Can't mount widget(s) before VerticalScroll() is mounted I would be grateful if someone would look at this problem a little longer. Another problem was also noticed here, which resulted in the premature closing of this issue. I still don't see a solution for dynamically updating the screen with no MountError or freeze :/ |
I know about the recent changes related to MountError in version
0.63.3
which is:However, it seems to me that something that worked before (and looks like it should still work) is not working with them.
I mean dynamically adding/removing widgets based on some changes occurring in a watched reactive.
We have observed this problem in our more extensive application, where we can reproduce it every time (I think because more things are going on - in the background data is being fetched from API), but the problematic code itself is quite simple to understand, so I will include it:
real app code looks like this
... so we watch for changes taking place in some reactive, and when it happens - we want to update the Cart, so we remove all existing items and create new ones.
There may be a better way to do this. If you have any suggestions, please don't hesitate. (but we don't want to
self.refresh(recompose=True)
as it has a visual effect because widgets above cart_items_container are also rebuilt).But after the latest textual bump, when such a sync occurs, we observe this error:
(When
self.watch
is commented - no issue occurs.self.__cart_items_container.mount(*new_cart_items)
is the problematic part not included in stacktrace.)real app traceback
This error is weird because we can see
widgets
is empty in locals. And what I thought about is that some race condition might happen between this__sync_cart_items
andcompose
which will result in "Can't mount widget(s) before CartItemsContainer() is mounted" (vs actual CartItem) but looks like that shouldn't happen because this watch is scheduled after initial compose (because in the on_mount method).MRE:
I sometimes manage to reproduce it using this code, but it's not easy, it requires multiple runs (if you can't still call it, you can modify the parameters like
labels_increment
and set_interval time)MRE
MRE exception
The strangest thing is that the error does not concern the container stored in init (which should be mounted anyway), but the elements that are for sure mounted (elements inside that container). This is strange because this error sounds as if it is not true - this situation does not occur at first glance.
The text was updated successfully, but these errors were encountered: