Coder Social home page Coder Social logo

Comments (12)

github-actions avatar github-actions commented on June 14, 2024

We found the following entries in the FAQ which you may find helpful:

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by FAQtory

from textual.

willmcgugan avatar willmcgugan commented on June 14, 2024

See the following advice about threaded workers https://textual.textualize.io/guide/workers/#posting-messages

I suspect fixing that will resolve your issue.

from textual.

max-arnold avatar max-arnold commented on June 14, 2024

Yep, that fixed the problem! Sorry for the noise.

from textual.

github-actions avatar github-actions commented on June 14, 2024

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

from textual.

max-arnold avatar max-arnold commented on June 14, 2024

Updated the code to send a Message, but I still get race conditions when navigating around the table (rarely) or holding Ctrl+R for a couple of seconds (more often). The end result is an empty Markdown widget and refresh doesn't help anymore:

CleanShot 2024-04-13 at 18 18 45@2x
import json

from textual import work
from textual.app import App, ComposeResult
from textual.message import Message
from textual.screen import Screen
from textual.widgets import Header, Footer, DataTable, Markdown
from textual.binding import Binding

import time, datetime


class AccountsListScreen(Screen[None]):
    BINDINGS = [
        ("ctrl+r", "refresh", "Refresh"),
    ]

    CSS = """
    Markdown {
    margin: 0 0 0 0;
    padding: 1 1 0 1;
    border: solid $panel-lighten-3;
    }
    """
    account: str | None = None
    accounts: dict[str, dict] = {}

    def compose(self) -> ComposeResult:
        yield Header()
        yield DataTable(cursor_type="row")
        yield Markdown(id="detail")
        yield Footer()

    def on_mount(self) -> None:
        self.sub_title = "Accounts"
        table = self.query_one(DataTable)
        table.add_columns("Name", "Instance", "Status", "Created at", "Updated at")
        self.refresh_accounts()

    def action_refresh(self) -> None:
        self.refresh_accounts()

    def on_data_table_row_highlighted(self, row: DataTable.RowSelected) -> None:
        self.account = row.row_key.value
        detail = self.query_one("#detail")
        if self.account:
            acc = self.accounts[self.account].copy()
            acc["created_at"] = acc["created_at"].strftime('%Y-%m-%d %H:%M:%S')
            acc["updated_at"] = acc["updated_at"].strftime('%Y-%m-%d %H:%M:%S')
            MD = f"""
* Name: {acc["name"]}
* Instance: {acc["instance"]}
* Status: {acc["status"]}
* Created at: {acc["created_at"]}
* Updated at: {acc["updated_at"]}

### Params
```json
{json.dumps(acc, indent=2)}
```
""".strip()
            detail.update(MD)
            detail.border_title = self.account
        else:
            detail.update("")
            detail.border_title = ""

    class DBReturn(Message):
        def __init__(self, accounts) -> None:
            self.accounts = accounts
            super().__init__()

    #@work(exclusive=True)
    def refresh_accounts(self) -> None:
        table = self.query_one(DataTable)
        # if table.loading:
        #     return
        table.loading = True
        self.get_accounts()

    @work(exclusive=True, thread=True)
    def get_accounts(self) -> None:
        time.sleep(1)  # synchronous DB call
        accounts = [
            {
                "name": f'Test Account {i}',
                "instance": "default",
                "status": "active",
                "created_at": datetime.datetime.now(),
                "updated_at": datetime.datetime.now(),
            }
            for i in range(10)
        ]
        # self.app.call_from_thread(self.on_accounts_list_screen_dbreturn, accounts)
        self.post_message(self.DBReturn(accounts))

    # @work(exclusive=True)
    def on_accounts_list_screen_dbreturn(self, accounts):
        table = self.query_one(DataTable)
        table.clear()
        self.account = None
        self.accounts = {}
        for acc in accounts.accounts:
            table.add_row(
                acc["name"],
                acc["instance"],
                acc["status"],
                acc["created_at"].strftime('%Y-%m-%d %H:%M:%S'),
                acc["updated_at"].strftime('%Y-%m-%d %H:%M:%S'),
                key=acc["name"],
            )
            self.accounts[acc["name"]] = acc
            self.account = self.account or acc["name"]
        table.border_title = f"{len(accounts.accounts)} accounts"
        table.loading = False
        table.focus()


class BackofficeApp(App):
    MODES = {
        "accounts": AccountsListScreen,
    }

    BINDINGS = [
        ("q", "quit()", "Quit"),
        Binding("f12", "take_screenshot()", "Take screenshot", show=False),
    ]

    CSS = """
    DataTable {
    border: solid $accent;
    }
    """

    def on_mount(self) -> None:
        self.title = "Title"
        self.switch_mode("accounts")

    def action_take_screenshot(self) -> None:
        filename = self.save_screenshot()
        self.notify(f"Saved in {filename}", title="Screenshot saved")


app = BackofficeApp()

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

from textual.

TomJGooding avatar TomJGooding commented on June 14, 2024

That's quite a bit of code to wade through, but I don't understand why you have so many workers and async handlers?

from textual.

max-arnold avatar max-arnold commented on June 14, 2024

I tried with only one worker (the threaded one), but then wrapped the remaining two callbacks into async ones to see if that helps (it didn't). I just updated the code example so it no longer has the extra workers.

UPD: also fixed the formatting issues

from textual.

willmcgugan avatar willmcgugan commented on June 14, 2024

Threaded workers cannot be forced to cancel (a limit of threads). Which means that you need to manually check if the worker is cancelled.

I've made a couple of tweaks to your code, and I can't reproduce that issue now:

import json

from textual import work
from textual.app import App, ComposeResult
from textual.message import Message
from textual.screen import Screen
from textual.widgets import Header, Footer, DataTable, Markdown
from textual.binding import Binding
from textual.worker import get_current_worker

import time, datetime


class AccountsListScreen(Screen[None]):
    BINDINGS = [
        ("ctrl+r", "refresh", "Refresh"),
    ]

    CSS = """
    Markdown {
    margin: 0 0 0 0;
    padding: 1 1 0 1;
    border: solid $panel-lighten-3;
    }
    """
    account: str | None = None
    accounts: dict[str, dict] = {}

    def compose(self) -> ComposeResult:
        yield Header()
        yield DataTable(cursor_type="row")
        yield Markdown(id="detail")
        yield Footer()

    def on_mount(self) -> None:
        self.sub_title = "Accounts"
        table = self.query_one(DataTable)
        table.add_columns("Name", "Instance", "Status", "Created at", "Updated at")
        self.refresh_accounts()

    def action_refresh(self) -> None:
        self.refresh_accounts()

    def on_data_table_row_highlighted(self, row: DataTable.RowSelected) -> None:
        self.account = row.row_key.value
        detail = self.query_one("#detail")
        if self.account:
            acc = self.accounts[self.account].copy()
            acc["created_at"] = acc["created_at"].strftime("%Y-%m-%d %H:%M:%S")
            acc["updated_at"] = acc["updated_at"].strftime("%Y-%m-%d %H:%M:%S")
            MD = f"""
* Name: {acc["name"]}
* Instance: {acc["instance"]}
* Status: {acc["status"]}
* Created at: {acc["created_at"]}
* Updated at: {acc["updated_at"]}

### Params
```json
{json.dumps(acc, indent=2)}
```
""".strip()
            detail.update(MD)
            detail.border_title = self.account
        else:
            detail.update("")
            detail.border_title = ""

    class DBReturn(Message):
        def __init__(self, accounts) -> None:
            self.accounts = accounts
            super().__init__()

    # @work(exclusive=True)
    def refresh_accounts(self) -> None:
        table = self.query_one(DataTable)
        # if table.loading:
        #     return
        table.loading = True
        self.get_accounts()

    @work(exclusive=True, thread=True)
    def get_accounts(self) -> None:
        time.sleep(1)  # synchronous DB call
        accounts = [
            {
                "name": f"Test Account {i}",
                "instance": "default",
                "status": "active",
                "created_at": datetime.datetime.now(),
                "updated_at": datetime.datetime.now(),
            }
            for i in range(10)
        ]
        # self.app.call_from_thread(self.on_accounts_list_screen_dbreturn, accounts)
        if not get_current_worker().is_cancelled:
            self.post_message(self.DBReturn(accounts))

    # @work(exclusive=True)
    def on_accounts_list_screen_dbreturn(self, accounts):
        table = self.query_one(DataTable)
        table.clear()
        self.account = None
        self.accounts = {}
        for acc in accounts.accounts:
            table.add_row(
                acc["name"],
                acc["instance"],
                acc["status"],
                acc["created_at"].strftime("%Y-%m-%d %H:%M:%S"),
                acc["updated_at"].strftime("%Y-%m-%d %H:%M:%S"),
                key=acc["name"],
            )
            self.accounts[acc["name"]] = acc
            self.account = self.account or acc["name"]
        table.border_title = f"{len(accounts.accounts)} accounts"
        table.loading = False
        table.focus()


class BackofficeApp(App):
    MODES = {
        "accounts": AccountsListScreen,
    }

    BINDINGS = [
        ("q", "quit()", "Quit"),
        Binding("f12", "take_screenshot()", "Take screenshot", show=False),
    ]

    CSS = """
    DataTable {
    border: solid $accent;
    }
    """

    def on_mount(self) -> None:
        self.title = "Title"
        self.switch_mode("accounts")

    def action_take_screenshot(self) -> None:
        filename = self.save_screenshot()
        self.notify(f"Saved in {filename}", title="Screenshot saved")


app = BackofficeApp()

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

from textual.

max-arnold avatar max-arnold commented on June 14, 2024

I tried your version and the Ctrl+R problem seems to be gone. Thanks!

But the Markdown widget is still get blanked out when I repeatedly press UP and DOWN keys:

2024-04-14 17 56 47

Wrapping the on_data_table_row_highlighted into an async worker doesn't help. Is something tricky required to prevent this as well?

from textual.

TomJGooding avatar TomJGooding commented on June 14, 2024

Here's a quick MRE for the issue described above when rapidly updating the Markdown widget. Presumably the problem is that parsing the content is quite an expensive operation - is the best solution to await the return from Markdown.update?

from textual import on
from textual.app import App, ComposeResult
from textual.widgets import DataTable, Markdown

EXAMPLE_MARKDOWN = """\
## Try continuously scrolling the table by holding the down key

This is an example of Textual's `Markdown` widget.

Markdown syntax and extensions are supported.

- Typography *emphasis*, **strong**, `inline code` etc.
- Headers
- Lists (bullet and ordered)
- Syntax highlighted code blocks
- Tables!
"""


class ExampleApp(App):
    CSS = """
    DataTable, Markdown {
        height: 50%;
    }
    """

    def compose(self) -> ComposeResult:
        yield DataTable(cursor_type="row")
        yield Markdown()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.add_column("Column")
        for i in range(100):
            table.add_row(f"Row #{i}")

    @on(DataTable.RowHighlighted)
    def update_markdown(self, event: DataTable.RowHighlighted) -> None:
        markdown = self.query_one(Markdown)
        markdown.update(f"# Row #{event.cursor_row}\n{EXAMPLE_MARKDOWN}")


if __name__ == "__main__":
    app = ExampleApp()
    app.run()

from textual.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.