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

Fixed widget IDs to be reusable after window closes #2517

Merged
merged 8 commits into from
Apr 29, 2024

Conversation

proneon267
Copy link
Contributor

Fixes #2514

Fixed widget IDs to be reusable after window closes. Also added a new test to check the behavior.

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

I believe you've correctly identified the source of the bug here; but both the fix and the test miss a lot of details.

@@ -272,6 +272,9 @@ def close(self) -> None:
undefined, except for :attr:`closed` which can be used to check if the window
was closed.
"""
if getattr(self, "content", None) is not None:
Copy link
Member

Choose a reason for hiding this comment

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

In what conditions can content not exist as an attribute? It's one of the first attributes set on Window

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry, I meant to do:

if self.content:

@@ -272,6 +272,9 @@ def close(self) -> None:
undefined, except for :attr:`closed` which can be used to check if the window
was closed.
"""
if getattr(self, "content", None) is not None:
self.content.clear()
Copy link
Member

Choose a reason for hiding this comment

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

Why is this needed? AFAICT, setting the window to None should be sufficient, and will propagate to all children. Clearing the children from content means the ownership hierarchy of content will be destroyed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I hadn't noticed _remove() also propagates to the children as well. Also, does ownership hierarchy matter when we are clearing all the widgets?

"""Widget IDs can be reused after the associated widget's window is closed."""

# Common widget IDs
common_widget_ids = {"content": "window_content", "label": "sample_label"}
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 see why this is a dictionary containing 2 constants, instead of just using 2 constants. The "dictionary" features of this are completely unused - we just need 2 constant strings.

id=common_widget_ids["content"],
children=[toga.Label(text="Sample Label", id=common_widget_ids["label"])],
)
# Show the second window and check that it is in the app's windows registry.
Copy link
Member

Choose a reason for hiding this comment

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

There seems to be some confusion here. We aren't concerned about the window registry at all (not that there even is one. There's a list of windows).

What we care about is the widget registry. The tests you've got here verify the window has been created, but that's not the thing that is the source of the bugs here.

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

This is a lot closer. The implementation is simpler, and the tests are actually validating the thing that has been changed.

However, the tests are only verifying the "good" path through the code - there's no validation of the "bad" path. Consider the cases of:

  1. Adding the a widget with a re-used ID to an existing (and visible) window
  2. Constructing a new window that re-uses an ID

It should be legal to create a widget with a duplicated ID; it's only when the widget is added to a window that a problem will occur (as widgets don't appear in the registry until they're part of a layout).

There's also a difference between a parent widget with a duplicated ID and a child widget with a duplicated ID. Both will raise an error - but the paths to generating that error may be different.

@proneon267
Copy link
Contributor Author

I also noticed another bug while testing: windows can have the same IDs, which defeats the purpose of ensuring uniqueness. Additionally, I didn't find any apparent way to retrieve a window by its ID, other than by iterating through all windows with the following code:

for window in app.windows:
    if window.id == CHOSEN_WINDOW_ID:
        # Found the window
        break

Is this the correct approach, or am I missing something?
If not, could we make the Window Registry API symmetrical to the Widget Registry? This would ensure that only windows with unique IDs are added to the windows registry, and also allow us to retrieve a window by its ID using:

app.windows[CHOSEN_WINDOW_ID]

@freakboy3742
Copy link
Member

.. could we make the Window Registry API symmetrical to the Widget Registry? This would ensure that only windows with unique IDs are added to the windows registry, and also allow us to retrieve a window by its ID using:

I can't see any reason we shouldn't allow lookup-by-ID for windows; in fact, most (if not all) of the implementation of WidgetRegistry should be reusable as well.

However, that's new feature, not a bug, and it should be handled independently of this PR.

Copy link
Member

@freakboy3742 freakboy3742 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 corrections on how you're using (or not using) pytest test mechanisms.

try:
third_window_content = toga.Box(id=CONTENT_WIDGET_ID)
except KeyError:
raise AssertionError(
Copy link
Member

Choose a reason for hiding this comment

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

This would usually be pytest.fail(), not a manual raise of AssertionError.

except KeyError:
assert True
else:
raise AssertionError(
Copy link
Member

Choose a reason for hiding this comment

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

This whole block should be with pytest.raises.

try:
new_label_widget = toga.Label(text="Sample Label", id=LABEL_WIDGET_ID)
except KeyError:
raise AssertionError(
Copy link
Member

Choose a reason for hiding this comment

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

As above, this is pytest.fail()

except KeyError:
assert True
else:
raise AssertionError(
Copy link
Member

Choose a reason for hiding this comment

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

As above, this is with pytest.raises

@proneon267
Copy link
Contributor Author

Thanks for the tips. The with pytest.raises is less verbose than try except else blocks, but I didn't notice any way to specify the custom error message with with pytest.raises. Is that the intended behavior or am I missing something?

@freakboy3742
Copy link
Member

Thanks for the tips. The with pytest.raises is less verbose than try except else blocks, but I didn't notice any way to specify the custom error message with with pytest.raises. Is that the intended behavior or am I missing something?

with pytest.raises(ValueError, match=r"You forgot to include .* in your arguments"`:
    ...

The match argument is a regular expression that will be compared against the message of the exception raised. There's no need to customise the message of the test - the default return value is clear enough.

@proneon267
Copy link
Contributor Author

Thanks, I have changed the test to use pytest helpers. Let me know if any other changes are needed. I am currently working on #2484 and will start a new PR for changing the window registry implementation when #2484 will near completion.

Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

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

I've made a couple more tweaks to the test to protect against some remaining edge cases, and make the exception tests a little more robust; but otherwise this looks good. Thanks for the fix!

While I don't want to dampen your enthusiasm, I would be wary of having too many open PRs at once. We definitely appreciate both bug fixes and feature improvements, but the PRs you're currently producing consistently require multiple passes and improvements before they get to a final form. We're happy to provide the feedback needed to land these PRs; but when you have multiple PRs in flight at once, it means we (the maintainers/reviewers) end up spending a lot of time providing the same sort of feedback on multiple PRs, rather than having getting one PR to completion, then incorporating the experience you've gained in landing one PR into making the next PR better.

By my count, you currently have 7 open PRs; I'd vastly prefer to see these land before you start opening new PRs - especially if you're proposing adding a feature.

@freakboy3742 freakboy3742 merged commit 5242026 into beeware:main Apr 29, 2024
34 checks passed
@proneon267
Copy link
Contributor Author

I agree with you. I will complete the remaining PRs first and during this time, I will only open a new PR if any of them will be necessary for landing the existing open PRs. Thank you helping as always 😄

@proneon267 proneon267 deleted the widget_id_reusable_fix branch April 29, 2024 05:11
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.

Widget IDs are not cleared when a window closes
2 participants