textual icon indicating copy to clipboard operation
textual copied to clipboard

watch_ method not working with dict

Open aaronst opened this issue 3 years ago • 4 comments

Sorry if the title is off. I'm having a hard time getting watch_ methods working with dict attributes. Here's an example:

from json import dumps
from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widgets import Input, Static


class Example(App):
    display = reactive({"input": ""})

    def compose(self) -> ComposeResult:
        yield Input()
        yield Static()

    def on_input_changed(self, event: Input.Changed) -> None:
        self.display["input"] = event.value

    def watch_display(self, new_display: dict) -> None:
        self.query_one(Static).update(dumps(new_display))


Example().run()

As I type in the Input, the Static's content doesn't update. Interestingly, if I swap json.dumps() for rich.Pretty(), it does update, but only after I move the mouse over it. The only way I've been able to get what I want is by moving the update() into the on_input_changed() handler.

    def on_input_changed(self, event: Input.Changed) -> None:
        self.display["input"] = event.value
        self.query_one(Static).update(dumps(self.display))

aaronst avatar Nov 02 '22 22:11 aaronst

Hi @aaronst. The issue you've got here is that, at least for now, a Reactive can't know that you've modified the content of a container type. So, for example, if you modify the content of a dict, list, or a property of an instance of a class, the reactive can't know that you've changed something inside that container -- it can only know if you make changes to the reactive property itself.

Without knowing what it is you're trying to achieve in your wider code. Your example above could be reduced to just:

from textual.app import App, ComposeResult
from textual.widgets import Input, Static

class Example(App):

    def compose(self) -> ComposeResult:
        yield Input()
        yield Static()

    def on_input_changed(self, event: Input.Changed) -> None:
        self.query_one(Static).update(event.value)

Example().run()

Or if you do want a reactive, I think I'd have a reactive for the value itself (note that I've called it display_val rather than display as the latter is a property of items in the Textual DOM so that would create a clash):

from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widgets import Input, Static


class Example(App):

    display_val = reactive("")

    def compose(self) -> ComposeResult:
        yield Input()
        yield Static()

    def on_input_changed(self, event: Input.Changed) -> None:
        self.display_val = event.value

    def watch_display_val(self, new_display: str) -> None:
        self.query_one(Static).update(new_display)


Example().run()

Finally, there is a change to Textual coming (it should be in 0.4.0) that would allow you to do what you want, with a very small workaround. A reactive can have an always_update attribute set to True and it will always update when assigned to. Then you could reassign the reactive to itself to force a watch_ event to happen. For example:

from json import dumps
from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widgets import Input, Static


class Example(App):
    display_val = reactive({"input": ""}, always_update=True)

    def compose(self) -> ComposeResult:
        yield Input()
        yield Static()

    def on_input_changed(self, event: Input.Changed) -> None:
        self.display_val["input"] = event.value
        # WORKAROUND: Assign the reactive to itself to force a watch_.
        self.display_val = self.display_val

    def watch_display_val(self, new_display: dict) -> None:
        self.query_one(Static).update(dumps(new_display))


Example().run()

Finally: the effect you were seeing with the Rich pretty-print facility is a bit of unintended behaviour that we'll be looking into (long story short: it's not intended to work the way you're seeing).

davep avatar Nov 03 '22 11:11 davep

Ahh, okay, really appreciate the writeup! I thought maybe that was the case with dict, but seeing the Pretty() update had me convinced that it was at least partially working. Thanks again, really been enjoying learning and working with all the new capability.

aaronst avatar Nov 03 '22 15:11 aaronst

Should I keep this open for the unintended behavior mentioned? Otherwise I'll close it out.

aaronst avatar Nov 05 '22 07:11 aaronst

Aye, it's fine to close if you're happy with that.

davep avatar Nov 05 '22 10:11 davep

Did we solve your problem?

Glad we could help!

github-actions[bot] avatar Nov 05 '22 19:11 github-actions[bot]