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
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.

2 participants