Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Delayed initialization in reactivity #4006

Closed
jakubziebin opened this issue Jan 11, 2024 · 0 comments
Closed

Delayed initialization in reactivity #4006

jakubziebin opened this issue Jan 11, 2024 · 0 comments

Comments

@jakubziebin
Copy link

Hi, I have some problem with reactivity. I want to have a reactive data container like dict, dataclass, etc..
But then I can't instantiate it in right time, because it is already forced to be instantiated in the reactive() call.
Which means that all the properties in my dataclass must have default values.

But I don't want that. Because in code which uses these properties in some scenarios I need to have a unnecessary
if/else statement to check if the actual data is already there or not.

This is more or less how it looks in the code:

import random
from dataclasses import dataclass

from textual.app import App, ComposeResult
from textual.reactive import var
from textual.widgets import RichLog


@dataclass(frozen=True)
class DataHolder:
    value: int | None = None
    values: list[int] | None = None  # in a real case could be an empty list, so I can't use that as default value


class FirstApp(App[None]):
    content: DataHolder = reactive(DataHolder(), init=False)

    @property
    def rich_log(self) -> RichLog:
        return self.query_one(RichLog)

    def on_mount(self) -> None:
        self.set_interval(1, self.update_content)
        self.watch(self, "content", self.content_has_been_changed, init=False)

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

    def update_content(self):
        def randomize() -> int:
            return random.randint(1, 100)

        self.rich_log.write("Updating content...")
        self.content = DataHolder(
            value=randomize(),
            values=[randomize() for _ in range(3)],
        )

    def content_has_been_changed(self):
        if self.content.value is not None:  # But we re sure that it is always not None
            self.rich_log.write(f"a: {self.content.value=}")

        if self.content.values is not None:  # But we re sure that it is not None
            self.rich_log.write(f"b: {self.content.values=}")

        # So we should be able to use it like this, but typing doesn't know that as it is `| None`
        self.rich_log.write(f"c: {self.content.value=}")
        self.rich_log.write(f"d: {self.content.values=}")


FirstApp().run()

I hope the example above is clear enough. It is a simple example in which also removing | None should do the job:

value: int = -1
values: list[int] = field(default_factory=list)

but in a more complex case it is not possible and...

I wanted to ask is there a way to have that reactive later evaluated? I mean I don't want to instantiate
the DataHolder class right in the reactive(), but I want to set it later, when my data arrives. Is that possible?
Maybe should be with some syntax that is common in python (e.g. in Typer) I mean ... to mark it like that:

content: DataHolder = reactive(..., init=False)

What I actually tried is a workaround like:

content: DataHolder = reactive(None, init=False)

but this is not a good solution, because it won't throw an error if someone try to access content by accident before
it is set. Neither a good solution is to use | None in the dataclass, because then I need to have a if/else
statement in every place where I use it:

content: DataHolder | None = reactive(None, init=False)

A good analogy is the way how to handle it via @Property:

@dataclass
class SomeDataClass:
    _content: DataHolder | None = None

    @property
    def content(self) -> DataHolder:
        if self._content is None:
            raise ValueError("Content is not set yet")
        return self._content

    def is_content_set(self) -> bool:
        return self._content is not None

I think same thing should be possible with reactive() as well but we cannot have one reactive depend on another one.

I hope I was clear enough. Thanks in advance for any help.

@Textualize Textualize deleted a comment from github-actions bot Jan 11, 2024
@Textualize Textualize locked and limited conversation to collaborators Jan 11, 2024
@davep davep converted this issue into discussion #4007 Jan 11, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant