Skip to content

fix(cli,session): remove nest_asyncio, make chat-profile config async-safe#2953

Open
pidefrem wants to merge 6 commits into
Chainlit:mainfrom
pidefrem:fix/remove-nest-asyncio-python-3-14
Open

fix(cli,session): remove nest_asyncio, make chat-profile config async-safe#2953
pidefrem wants to merge 6 commits into
Chainlit:mainfrom
pidefrem:fix/remove-nest-asyncio-python-3-14

Conversation

@pidefrem

@pidefrem pidefrem commented Jun 8, 2026

Copy link
Copy Markdown

Summary

Problem 1 — nest_asyncio breaks Python 3.14

nest_asyncio.apply() is called at import time in chainlit/cli/__init__.py.
nest_asyncio <= 1.6.0 internally calls asyncio.ensure_future(future, loop=self),
where the loop= keyword was removed in Python 3.14 (bpo-39529).
This corrupts asyncio task registration, causing asyncio.current_task() to return
None inside running coroutines.

The symptom is a white page on every Chainlit app running on Python 3.14:
starlette's FileResponse calls anyio.to_thread.run_sync(), which calls
sniffio.current_async_library(), which calls asyncio.current_task() -> None
-> anyio.NoEventLoopError -> HTTP 500 on every static asset.

Problem 2 — get_config() relied on nest_asyncio for re-entrant event loop

WebsocketSession.get_config() called asyncio.get_event_loop().run_until_complete()
from within the already-running event loop (the SocketIO connect handler).
This only worked because nest_asyncio patched run_until_complete to be re-entrant.
Removing nest_asyncio without fixing this would silently break chat-profile
config overrides on all Python versions (3.10-3.13), not just 3.14.

Fix

Commit 1 (b1a6bb4): Remove nest_asyncio entirely.
asyncio.run(start()) is a top-level entry point; it does not need re-entrant
loop support. Also removes the nest-asyncio dependency from pyproject.toml.
Adds a regression test to prevent silent reintroduction.

Commit 2 (55bf45b): Exclude .pytest_cache from Prettier checks.

Commit 3 (2406b99): Make chat-profile config resolution async-safe.

  • get_config() now returns the cached config immediately (no run_until_complete)
  • New async resolve_config() method properly awaits set_chat_profiles()
  • connect() handler calls await session.resolve_config() after construction
  • 9 new tests covering overrides, idempotency, error handling, unknown profiles,
    and a regression test verifying run_until_complete is never called
  • Adds hatchling to the mypy optional deps (it ships py.typed;
    fixes a pre-existing import-not-found mypy error in build.py)

Testing

All 798 tests pass on Python 3.10, 3.11, 3.12, and 3.13.
Python 3.14 cannot be tested yet (pydantic-core lacks a 3.14 wheel).

Note: the requires-python < 3.14 upper bound is intentionally left unchanged.
Lifting it is a separate decision once the full dependency chain supports 3.14.

Refs #2767


Known pre-existing issue (out of scope)

cypress/support/run.ts:27 has a bug: access() from fs/promises returns a
Promise (always truthy), so the if (!access(entryPointPath)) check is dead code
and the file-existence guard never triggers. This predates this PR and should be
addressed separately.

nest_asyncio <=1.6.0 patches loop.run_until_complete() via
asyncio.ensure_future(future, loop=self). The loop= keyword was
deprecated in Python 3.8 and removed in Python 3.14 (bpo-39529).
Calling nest_asyncio.apply() at import time silently corrupted asyncio
task registration: asyncio.current_task() returned None inside running
coroutines, causing anyio.NoEventLoopError on every static-asset
request (HTTP 500 / white screen) on Python 3.14.

The fix removes nest_asyncio entirely. asyncio.run(start()) is a
top-level entry point and has never needed re-entrant loop support.
The existing comment in the code ("Not sure if it is necessary...")
confirms this usage was always uncertain.

Also removes the nest-asyncio dependency from pyproject.toml.

Adds a regression test to prevent reintroduction.

Refs Chainlit#2767
@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. backend Pertains to the Python backend. dependencies Pull requests that update a dependency file labels Jun 8, 2026

@cubic-dev-ai cubic-dev-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.

No issues found across 4 files

Re-trigger cubic

AJ1035951 added 2 commits June 8, 2026 15:43
Replace the synchronous get_config() call to
asyncio.get_event_loop().run_until_complete() with a new
async resolve_config() method.  The old approach required
nest_asyncio to work inside the already-running event loop
and silently failed without it.

- get_config() now returns the cached config immediately
- resolve_config() awaits set_chat_profiles() properly
- connect() handler calls await session.resolve_config()
- Add 9 tests covering overrides, idempotency, error handling,
  and a regression test verifying run_until_complete is not called
- Add hatchling to mypy optional deps (ships py.typed, fixes
  pre-existing import-not-found error in build.py)
@pidefrem pidefrem changed the title fix(cli): remove nest_asyncio to restore Python 3.14 compatibility fix(cli,session): remove nest_asyncio, make chat-profile config async-safe Jun 8, 2026
nest_asyncio was removed from chainlit's direct dependencies in the
previous commit.  The ignore_missing_imports override in the root
pyproject.toml mypy config is now dead — chainlit no longer imports
it anywhere.

nest_asyncio remains in uv.lock as a transitive dependency of
llama-index-core and semantic-kernel, so no lockfile change is needed.

@cubic-dev-ai cubic-dev-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.

1 issue found across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread cypress.config.ts Outdated
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Jun 9, 2026
@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open for 14 days with no activity.

@github-actions github-actions Bot added the stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed label Jun 24, 2026
@pidefrem

Copy link
Copy Markdown
Author

Not stale — this PR is active. I just pushed 012f46d1 (docstring clarification for resolve_config caching), and the branch is up to date with main with no conflicts. It fixes #2952.

It's been waiting on a maintainer review. Could someone take a look when they have a chance? Happy to rebase or address feedback. Thanks! 🙏

@github-actions github-actions Bot removed the stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed label Jun 27, 2026

@dokterbob dokterbob left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks! Some much welcomed cleanup/fixup.

@dokterbob dokterbob enabled auto-merge June 30, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Pertains to the Python backend. dependencies Pull requests that update a dependency file size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants