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

Rework ui_dispatch tests to avoid need for Qt #1792

Merged
merged 2 commits into from May 7, 2024

Conversation

mdickinson
Copy link
Member

Since #1788, we have only one test module that makes use of the Qt event loop. That test module contains tests for the behaviour of handlers that use dispatch='ui' mechanism to redispatch off-thread notifications to the ui thread.

This PR reworks that test module, with some significant collateral damage along the way.

In detail:

  • reworks that test module (test_ui_notifiers) to avoid the need for the Qt event loop; instead, it tests against a ui_handler based on asyncio, which redispatches to the running asyncio event loop
  • adds a get_ui_handler counterpart to set_ui_handler, and exposes both functions in traits.api
  • adds type hints for get_ui_handler and set_ui_handler
  • removes two public module globals from trait_notifiers: ui_handler has been made private, while ui_thread is removed altogether
  • fixes a bug where ui dispatch didn't do the right thing (PR MAINT: add main thread check for ui dispatch, solve no ui failure #1740 was incomplete; this bug should have been caught at review time on that PR)
  • makes another couple of drive-by cleanups, removing a very old check for threading.local() being a dict (which it hasn't been in living memory), and tidying up some uses of thread identity.

@@ -615,10 +614,10 @@ class FastUITraitChangeNotifyWrapper(TraitChangeNotifyWrapper):
"""

def dispatch(self, handler, *args):
if threading.current_thread().ident == ui_thread:
if threading.current_thread() == threading.main_thread():
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the fix that should have been part of #1740.

@@ -153,11 +159,7 @@ def _get_handlers(self):
thread.
"""
thread_local = self.thread_local
if isinstance(thread_local, dict):
Copy link
Member Author

Choose a reason for hiding this comment

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

Drive-by cleanup: this condition is never true in modern Python.

@mdickinson
Copy link
Member Author

@flongford Do you have bandwidth for review? This is part of a longer-term goal of insulating Traits from changes in Qt / PySide, and eventually decoupling Traits and TraitsUI entirely.

Copy link

@flongford flongford left a comment

Choose a reason for hiding this comment

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

LGTM!

Thanks for the explanation of the code changes @mdickinson. The approach to separate the concept of "a ui handler" from "Qt's ui handler" looks like what we want here. The asyncio package also seems like an appropriate choice for creating a a test "ui handler".

I could mostly follow along with the what's being changed, though I've left some comments for my own understanding.

"""
UI handler that dispatches to the asyncio event loop.
"""
event_loop.call_soon_threadsafe(lambda: handler(*args, **kwargs))

Choose a reason for hiding this comment

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

Neat!

Comment on lines 619 to +620
else:
ui_handler(handler, *args)
_ui_handler(handler, *args)

Choose a reason for hiding this comment

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

Should we also guard against _ui_handler being undefined here, as we do in ui_dispatch?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we definitely should! Now I have to go figure out why the tests aren't exercising this ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm. I think they are exercising this, but our tests for the exception don't distinguish between a RuntimeError with a nice message and a TypeError from trying to use None as a callable. I need to re-raise exceptions and catch them ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Reworked in cc8653e

traits/trait_notifiers.py Show resolved Hide resolved
""" Tests for dynamic notifiers with `dispatch='ui'` set by method call.
"""
class TestMethodUINotifiers(
BaseTestUINotifiers, unittest.IsolatedAsyncioTestCase

Choose a reason for hiding this comment

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

Huh, TIL about unittest.IsolatedAsyncioTestCase!


obj = self.obj_factory()
def test_notification_from_separate_thread_failure_case(self):

Choose a reason for hiding this comment

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

I think I understand the context for this test case, but just to be clear: do we accept that any background process unknown to the UI will be able to modify objects, but ideally we also want to know about it?

If so, is this a new edge case that we can just test more easily without Qt, or does this happen often?

Copy link
Member Author

Choose a reason for hiding this comment

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

So the key point of the test (and of the dispatch="ui" functionality) is that even though the object is modified off the main thread, the trait change handlers are run in the main UI thread.

That said, this pattern isn't ideal in the first place; my preference is to use something like Traits Futures so that we're not modifying the same object from multiple threads in the first place.

Not sure whether this answers your question or not ... ?

Choose a reason for hiding this comment

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

Ah, I see - so the main result of this test is that we don't see any notifiers and we observe an exception, since the handlers are always on the main UI tread.

That helps understand the context, yes, thanks!

Copy link
Member Author

Choose a reason for hiding this comment

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

Right - the notifiers don't get executed at all in this case.

@mdickinson
Copy link
Member Author

@flongford I've responded to comments above and made one minor update, for better exception handling.

@mdickinson mdickinson requested a review from flongford May 3, 2024 13:39
@mdickinson mdickinson merged commit 2bc548f into main May 7, 2024
29 checks passed
@mdickinson mdickinson deleted the dev/rework-tests-to-avoid-qt branch May 7, 2024 09:46
mdickinson added a commit that referenced this pull request May 7, 2024
Since #1788 and #1792, we should no longer need a working Qt backend
when testing. Accordingly, this PR:

- removes PySide6 as a test dependency
- removes the need to `apt-get` Qt packages on Ubuntu runners
- removes the use of `xvfb-run` on Ubuntu runners
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.

None yet

2 participants