Textualize/textual

Mounting of `Markdown` isn't reliably awaitable

Closed this issue · 3 comments

davep commented

I imagine this would be an issue for any container-like widget that itself returns awaitables of some description, but I first encountered this with Markdown and the attempt to populate a VerticalScroll with them, awaiting a mount_all of them into the VerticalScroll, and then attempting to scroll_end.

The effect of the problem can be seen with this code:

from textual.app import App, ComposeResult
from textual.containers import VerticalScroll
from textual.widgets import Markdown

MARKDOWN = """

## Have some code:

```lisp
(defconstant +byte+ '(unsigned-byte 8)
  "Type of a byte array element.")

(defconstant +2bit-version+ 0
  "The only valid version number of 2bit data.")

(defconstant +signature+ #x1a412743
  "2bit file signature.")

(defconstant +bases+ #("T" "C" "A" "G")
  "Vector of the bases.

Note that the positions of each base in the vector map to the 2bit decoding
for them.")
```

## Now a list:

- This
- Is
- A
- List
- Really
"""

class ScrollEndWithMarkdownApp(App[None]):

    def compose(self) -> ComposeResult:
        yield VerticalScroll()

    async def on_mount(self) -> None:
        await self.query_one(VerticalScroll).mount_all([
            Markdown(f"# This is Markdown {n}{MARKDOWN}") for n in range(50)
        ])
        self.query_one(VerticalScroll).scroll_end(animate=False)

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

When run it should await the mount_all of a number of Markdown into the VerticalScroll and then perform a scroll_end. The result is that it scrolls almost, but not quite, to the end:

Screenshot 2024-04-28 at 09 06 39

Delaying the scroll_end using call_next, call_later or call_after_refresh makes no difference. So far the only approach I've found to make this happen is to use a set_timer to delay the scroll_end, and the time required seems to be proportional to the number of Markdown within the VerticalScroll (on my Mac Mini, M2Pro, under normal load, anything less than a 0.9 second delay results in it not scrolling to the end; in the reasonable use case in the application I ran into this in I'm having to use a 0.05 second delay).

textual diagnose output

Textual Diagnostics

Versions

Name Value
Textual 0.58.0
Rich 13.7.1

Python

Name Value
Version 3.10.13
Implementation CPython
Compiler Clang 15.0.0 (clang-1500.0.40.1)
Executable /Users/davep/develop/python/textual-sandbox/.venv/bin/python

Operating System

Name Value
System Darwin
Release 23.4.0
Version Darwin Kernel Version 23.4.0: Fri Mar 15 00:12:49 PDT 2024; root:xnu-10063.101.17~1/RELEASE_ARM64_T6020

Terminal

Name Value
Terminal Application Kitty
TERM xterm-256color
COLORTERM truecolor
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

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

I might be wrong but is the problem that the content of Markdown is updated after it is mounted? I seem to remember the update was awaited once upon a time, as the parsing seems an expensive operation.

def _on_mount(self, _: Mount) -> None:
if self._markdown is not None:
self.update(self._markdown)

@TomJGooding is correct in where the issue lies. Changing that _on_mount to async and awaiting the update fixes this.

Don't forget to star the repository!

Follow @textualizeio for Textual updates.