Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,66 @@ jobs:
# Currently only Android supports colored output. See https://github.com/python/cpython/issues/150932 for iOS.
CIBW_TEST_COMMAND: python -m pytest ${{ matrix.config.platform == 'android' && '--color=yes' || '' }}

test-pyodide:
permissions:
contents: read # to fetch code (actions/checkout)

name: Test (pyodide, ${{ matrix.pyver }})
runs-on: ubuntu-latest
needs: gen_llhttp
env:
# Must be a version supported by the pinned cibuildwheel below.
PYODIDE_VERSION: 0.29.3
strategy:
matrix:
pyver: ["cp313"]
Comment on lines +384 to +387

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It would be great to also build and test for Pyodide 314.0.2 / Python 314.

steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Setup Python ${{ matrix.pyver }}
id: python-install
# important: do not use system python

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Would be nice to know why it's important.

env:
UV_PYTHON_PREFERENCE: only-managed
uses: astral-sh/setup-uv@v8.2.0
with:
python-version: ${{ matrix.pyver }}
activate-environment: true
enable-cache: true
- name: Install build tooling and cython
run: |
uv pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't see where we use wheel, setuptools, build, or twine?

Suggested change
uv pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt
uv pip install -U pip -r requirements/cython.in -c requirements/cython.txt

- name: Restore llhttp generated files
uses: actions/download-artifact@v8
with:
name: llhttp
path: vendor/llhttp/build/
- name: Cythonize
run: |
make cythonize
- name: Install cibuildwheel
run: uv pip install cibuildwheel==3.4.1
- name: Build wheel
# See the comment in test-mobile about unsetting GITHUB_ACTIONS.
run: env -u GITHUB_ACTIONS cibuildwheel --output-dir dist

@hoodmane hoodmane Jul 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@henryiii should cibuildwheel have some explicit way to opt out of folding?

cibuildwheel normally uses grouping in its outputs for its build/test steps. But the loading time when expanding large groups in GitHub Actions is very high. So by unsetting GITHUB_ACTIONS, cibuildwheel does not know that it is running in a GitHub Action and thus does not use groups.

env:
CIBW_BUILD: ${{ matrix.pyver }}-*
CIBW_PLATFORM: pyodide
CIBW_PYODIDE_VERSION: ${{ env.PYODIDE_VERSION }}
- name: Download Pyodide distribution
uses: pyodide/pyodide-actions/download-pyodide@v2
with:
version: ${{ env.PYODIDE_VERSION }}
to: pyodide-dist
- name: Install test dependencies
# aiohttp itself is needed on the host because tests/conftest.py
# imports it; the pure-Python build is sufficient.
run: AIOHTTP_NO_EXTENSIONS=1 uv pip install -e . pytest-pyodide==0.59.2 pytest pytest-aiohttp pytest-timeout coverage

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why do we pin pytest-pyodide? I would think we'd either want to pin all the deps or only ones that actually need a pin.

- name: Run tests in Pyodide (Node.js)
run: python -m pytest tests/test_pyodide.py --rt node --dist-dir=./pyodide-dist

autobahn:
permissions:
contents: read # to fetch code (actions/checkout)
Expand Down Expand Up @@ -556,6 +616,7 @@ jobs:
- lint
- test
- test-mobile
- test-pyodide
- autobahn

runs-on: ubuntu-latest
Expand Down
13 changes: 13 additions & 0 deletions .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,16 @@ ignore_missing_imports = True

[mypy-gunicorn.*]
ignore_missing_imports = True

# JavaScript FFI modules, only available inside Emscripten/Pyodide runtimes.
[mypy-js]
ignore_missing_imports = True

[mypy-pyodide.*]
ignore_missing_imports = True
Comment on lines +41 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Does it fix these type errors to install pyodide-py in the mypy job?


# The run_in_pyodide decorator comes from pytest.importorskip() because
# pytest-pyodide is only installed for the dedicated Pyodide CI job.
[mypy-test_pyodide]
disallow_any_decorated = False
disallow_untyped_decorators = False
Comment on lines +47 to +51

@hoodmane hoodmane Jul 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Does it fix this type error to install pytest-pyodide in the mypy job?

5 changes: 5 additions & 0 deletions CHANGES/7803.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Added client support for running under Pyodide / Emscripten (WebAssembly in
browsers and Node.js). :class:`~aiohttp.ClientSession` now automatically
uses the new :class:`~aiohttp.FetchConnector`, which dispatches requests
through the JavaScript ``fetch()`` API instead of raw sockets -- by
:user:`hoodmane` and :user:`bendichter`.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Austin Scola
Bai Haoran
Ben Bader
Ben Beasley
Ben Dichter
Ben Greiner
Ben Kallus
Ben Timby
Expand Down
2 changes: 2 additions & 0 deletions aiohttp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
get_payload,
payload_type,
)
from .pyodide import FetchConnector
from .resolver import AsyncResolver, DefaultResolver, ThreadedResolver
from .streams import EMPTY_PAYLOAD, DataQueue, EofStream, StreamReader
from .tracing import (
Expand Down Expand Up @@ -139,6 +140,7 @@
"ClientWSTimeout",
"ConnectionTimeoutError",
"ContentTypeError",
"FetchConnector",
"Fingerprint",
"InvalidURL",
"InvalidUrlClientError",
Expand Down
35 changes: 35 additions & 0 deletions aiohttp/_llhttp_wasm_shim.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* llhttp's api.c references external wasm_on_* callbacks whenever __wasm__
is defined; they are normally provided by JavaScript in llhttp's own
WebAssembly bundle. When llhttp is instead linked into a Python extension
compiled with Emscripten (Pyodide) nothing provides those symbols, which
makes the extension fail to load. aiohttp installs its own callbacks via
llhttp_init() and never uses the wasm_settings/llhttp_alloc() path, so
no-op definitions are sufficient to satisfy the linker. */
#ifdef __wasm__

#include "llhttp.h"

int wasm_on_message_begin(llhttp_t *p) { return 0; }

int wasm_on_url(llhttp_t *p, const char *at, size_t length) { return 0; }

int wasm_on_status(llhttp_t *p, const char *at, size_t length) { return 0; }

int wasm_on_header_field(llhttp_t *p, const char *at, size_t length) {
return 0;
}

int wasm_on_header_value(llhttp_t *p, const char *at, size_t length) {
return 0;
}

int wasm_on_headers_complete(llhttp_t *p, int status_code, uint8_t upgrade,
int should_keep_alive) {
return 0;
}

int wasm_on_body(llhttp_t *p, const char *at, size_t length) { return 0; }

int wasm_on_message_complete(llhttp_t *p) { return 0; }

#endif /* __wasm__ */
8 changes: 7 additions & 1 deletion aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,13 @@ def __init__(
)

if connector is None:
connector = TCPConnector(ssl_shutdown_timeout=ssl_shutdown_timeout)
if sys.platform == "emscripten":
# WebAssembly has no sockets; requests go through fetch().
from .pyodide import FetchConnector

connector = FetchConnector()
else:
connector = TCPConnector(ssl_shutdown_timeout=ssl_shutdown_timeout)
# Initialize these three attrs before raising any exception,
# they are used in __del__
self._connector = connector
Expand Down
Loading
Loading