Skip to content

fix: yield event loop after automatic UI updates - #6764

Open
PythBuster wants to merge 1 commit into
flet-dev:mainfrom
PythBuster:patch-3
Open

fix: yield event loop after automatic UI updates#6764
PythBuster wants to merge 1 commit into
flet-dev:mainfrom
PythBuster:patch-3

Conversation

@PythBuster

@PythBuster PythBuster commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Description

Synchronous generator event handlers use yield as an explicit boundary for intermediate UI updates. Although auto-update queued the corresponding UI patch, __auto_update() did not actually suspend execution.

The generator could therefore resume immediately, allowing blocking work such as time.sleep() to prevent the send loop from processing the queued update.

Return whether __auto_update() updated a control and yield control to the event loop when it did. This ensures the send loop gets an opportunity to dispatch the UI patch before the handler continues, preserving the expected intermediate update behavior of yield and refreshing the UI after handler auto-updates.

Test code

# Minimal test/reproduction code for reviewers, if applicable.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist

  • I signed the CLA.
  • I have performed a self-review of my own code.
  • My code follows the style guidelines of this project.
  • I have commented my code, particularly in hard-to-understand areas.
  • My changes generate no new warnings.
  • New and existing tests pass locally with my changes.
  • I have made corresponding documentation changes, if applicable.
  • I have added changelog entries for user-facing changes, if applicable.
  • I have updated release guide pages and website/sidebars.yml for breaking changes, removals, and deprecations, if applicable.

Screenshots

Additional details

Summary by Sourcery

Ensure automatic UI updates triggered after event handlers yield control to the event loop so queued patches are sent before the handler continues.

Bug Fixes:

  • Prevent synchronous generator event handlers from blocking dispatch of auto-updated UI patches by yielding to the event loop when a control is updated.

Enhancements:

  • Make the auto-update helper report whether a control was updated, enabling conditional yielding after events.

Synchronous generator event handlers use `yield` as an explicit boundary for intermediate UI updates. Although auto-update queued the corresponding UI patch, `__auto_update()` did not actually suspend execution.

The generator could therefore resume immediately, allowing blocking work such as `time.sleep()` to prevent the send loop from processing the queued update.

Return whether `__auto_update()` updated a control and yield control to the event loop when it did. This ensures the send loop gets an opportunity to dispatch the UI patch before the handler continues, preserving the expected intermediate update behavior of `yield` and refreshing the UI after handler auto-updates.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@FeodorFitsner

Copy link
Copy Markdown
Contributor

Thanks for the PR! The diagnosis is correct and the mechanism works. I checked out the branch and reproduced the behavior with a harness that mimics the socket transport (send_messageasyncio.Queue → background send loop in flet_socket_server.py):

without yield: ['queued', 'blocking start', 'blocking end', 'SENT']
with    yield: ['queued', 'SENT', 'blocking start', 'blocking end']

One asyncio.sleep(0) is enough: put_nowait() wakes the send loop's pending get() before the handler's own resume callback, and StreamWriter.write() pushes eagerly. Existing tests pass (test_events.py, test_skip_double_update.py: 20 passed).

A few things before this can be merged.

1. Lint fails

ruff check on the branch reports three errors, so CI will reject it:

E501   Line too long (101 > 88)                         session.py:558
E261   Insert at least two spaces before an inline comment
SIM102 Use a single `if` statement instead of nested `if` statements

line-length = 88 is set in sdk/python/pyproject.toml. SIM102 wants the conditions merged:

if (
    context.auto_update_enabled()
    and not context.was_update_called()
    and await self.__auto_update(control)
):
    # give the send loop a chance to flush the patch before the
    # handler resumes and possibly blocks
    await asyncio.sleep(0)

2. The fix misses explicit .update() in a generator

The yield only happens when auto-update fired, so a handler that updates explicitly and then blocks still stalls - same user-visible bug, other update path:

def handler(e):
    text.value = "step 1"
    text.update()      # was_update_called() -> True, so no yield
    yield
    time.sleep(0.2)    # patch sits in the queue the whole time
# -> ['queued', 'blocking start', 'blocking end', 'SENT']

Putting the yield at the generator boundary in base_control.py covers both paths, and keeps after_event() free of a concern that only applies to generators:

for _ in event_handler(e):
    await session.after_event(session.index.get(self._i))
    await asyncio.sleep(0)

The async-generator branch needs the same treatment - time.sleep() inside an async generator blocks the loop identically.

3. Minor notes

  • Return value semantics. True means update() was called, not that a patch was queued: patch_control() only sends when len(patch) > 1, so a no-op update returns True with nothing on the wire (I saw exactly that locally). Harmless - one wasted loop turn - but "so the queued UI update can be sent" overstates it. "Give the send loop a chance to flush" is more accurate.
  • New suspension point on every event. after_event() runs at the end of every dispatch and from three call sites in app.py, so this adds an interleaving point to sync handlers that previously ran to completion. Low risk, and moving the yield into the generator branches (point 2) avoids it entirely.
  • Only the socket transport benefits. pyodide_connection and flet_dart_bridge_server send synchronously, so the yield is a no-op there. Worth a word in the comment so it doesn't get "optimized" away later.
  • Test and changelog. test_skip_double_update.py has a ready-made harness for a regression test asserting the frame is drained at the yield boundary. A ### Bug fixes bullet in CHANGELOG.md would be good too.

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.

2 participants