Skip to content
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

External watch fix init #4030

Merged
merged 12 commits into from
Feb 8, 2024
Merged

External watch fix init #4030

merged 12 commits into from
Feb 8, 2024

Conversation

rodrigogiraoserrao
Copy link
Contributor

@rodrigogiraoserrao rodrigogiraoserrao commented Jan 16, 2024

Fixes #3878

When a new watcher is created, we invoke that single watcher instead of checking all of the reactive watchers.
To enable this, we extract two auxiliary functions to the module scope from the inner scope they were defined in.

Typing reported (correctly) that the membership check would never evaluate to 'True' because we were comparing apples with tuples of oranges and apples. 'watcher_list' contains tuples whose second element _might_ match the callback, so we need to go over the tuples and unpack them to figure out if the callback is there.
When programmatically creating a watcher to a reactive attribute, init only the new watcher instead of triggering all watchers.

Related issue: #3878
Comment on lines -368 to +381
if callback in watcher_list:
if any(callback == callback_from_list for _, callback_from_list in watcher_list):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was checking if it could find an apple in a list of tuples of oranges and apples.
Instead, we must unpack the tuples in the list and compare apples to apples.

@rodrigogiraoserrao rodrigogiraoserrao marked this pull request as ready for review January 16, 2024 15:06
Copy link
Member

@darrenburns darrenburns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of requests, mostly around trying to make sure the tests are more robust.

tests/test_reactive.py Outdated Show resolved Hide resolved
tests/test_reactive.py Outdated Show resolved Hide resolved
src/textual/reactive.py Show resolved Hide resolved
@willmcgugan
Copy link
Collaborator

@rodrigogiraoserrao Please follow up on this. Please also update the description to summarize your changes.

assert logs == ["test_1", "test_2_extra"]
app.query_one(SomeWidget).test_2 = 73
assert logs.count("test_2_extra") == 2
assert "test_2" in logs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure that the watcher fires exactly once instead of at least once.

Suggested change
assert "test_2" in logs
assert logs.count("test_2") == 1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the ordering guarantees here actually? Do we need to ensure that the new test_2_extra comes before/after the new test_2 in the logs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember ever seeing that we must call watchers in any specific order. Do we..?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that it's documented, but we should ensure it's consistent I suppose 🤷


app = MyApp()
async with app.run_test():
before = counter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why you do the before and after stuff here instead of assert counter == 1 before the assignment and then assert counter == 2 after the assignment? That would seem simpler and a more accurate test.

As it stands, this test wouldn't catch duplicate watchers firing on init.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was testing whether the assignment triggered a single watcher but I agree your suggestion yields a more accurate test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See 1e682c2 for the change you suggested + a fix to a bug that your suggestion uncovered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, looks good!

rodrigogiraoserrao and others added 2 commits February 7, 2024 16:34
Co-authored-by: Darren Burns <darrenburns@users.noreply.github.com>
self.holder = Holder()

def on_mount(self) -> None:
self.watch(self.holder, "attr", self.callback)
Copy link
Member

@darrenburns darrenburns Feb 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name/docstring says it's checking that we skip duplicate watchers, but the test no longer involves a duplicate watcher. It seems to just be checking the basic usage of watch, which is probably covered by another test.

I reckon this should be updated such that are two watchers defined with self.watch(self.holder, "attr", self.callback), and checking that the self.callback fires only once on init, and once again on setting the value of attr.

Perhaps more importantly, another test should probably cover this case: If I define one watcher as self.watch(self.holder, "attr", self.first_callback) and then another with self.watch(self.holder, "attr", self.second_callback). Do I see one run of self.first_callback followed by one run of self.second_callback on init? Then when I assign attr, do I see the another one run of each callback in the correct order?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for the first two paragraphs, I think I deleted the second callback when I was debugging the bug I mentioned in a previous commit and then forgot to add it again, thanks for spotting that...

As for the third paragraph... Do you mean something like the test test_external_watch_init_does_not_propagate_to_externals added in 8b349e3?

See first two paragraphs of #4030 (comment).
See third paragraph of #4030 (comment).
Copy link
Member

@darrenburns darrenburns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, thanks for making those changes

@rodrigogiraoserrao rodrigogiraoserrao merged commit 7c7dec0 into main Feb 8, 2024
20 checks passed
@rodrigogiraoserrao rodrigogiraoserrao deleted the external-watch-fix-init branch February 8, 2024 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

A watch on a reactive that is declared init=False fires watch method on init
3 participants