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

Feature: Native Emscripten support #3195

Merged
merged 54 commits into from Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
89bdcd6
first version of emscripten fetch
joemarshall Nov 10, 2023
2a77ea8
streaming works and tests okay
joemarshall Nov 15, 2023
e6e9bb4
made emscripten tests skip if pytest-pyodide is not installed
joemarshall Nov 15, 2023
0cc345c
tests
joemarshall Nov 15, 2023
ec011f2
tests
joemarshall Nov 15, 2023
0819efe
mypy and lint
joemarshall Nov 16, 2023
4810c8b
Merge branch 'emscripten_support' of github.com:joemarshall/urllib3 i…
joemarshall Nov 16, 2023
8e2164e
Merge branch 'main' of github.com:urllib3/urllib3 into emscripten_sup…
joemarshall Nov 16, 2023
667d54f
changelog
joemarshall Nov 16, 2023
a0a5709
mypy updates for changes in main
joemarshall Nov 16, 2023
5dd3c4a
error handling and tests for it
joemarshall Nov 16, 2023
034ad0e
fixed upload bugs
joemarshall Nov 17, 2023
51364a4
ci updates
joemarshall Nov 17, 2023
b45cf16
comment
joemarshall Nov 17, 2023
1a00b78
lint
joemarshall Nov 17, 2023
09feff0
ci updates
joemarshall Nov 17, 2023
30d8b69
fix to if statement
joemarshall Nov 17, 2023
a45b32f
actions fix
joemarshall Nov 17, 2023
d8fb8ad
install pyodide_build for nox
joemarshall Nov 17, 2023
3c4bb66
hopefully fix noxfile pyodide_build dependency
joemarshall Nov 17, 2023
2f89bae
3.8 fixes
joemarshall Nov 17, 2023
7862c11
get pyodide_build version without requiring it
joemarshall Nov 17, 2023
05c97dd
3.8 compatible type checking
joemarshall Nov 17, 2023
363a0f3
review changes
joemarshall Nov 17, 2023
f871d98
prettier and eslint
joemarshall Nov 17, 2023
e0d973b
made tests less verbose
joemarshall Nov 17, 2023
5aaaab6
simplified multiple use of decorators
joemarshall Nov 18, 2023
5e4b9ae
simplified class passed to testserver_http
joemarshall Nov 18, 2023
319d067
ci action updates
joemarshall Nov 18, 2023
7918f0c
firefox emscripten tests
joemarshall Nov 18, 2023
611e16d
test checks console output
joemarshall Nov 18, 2023
242af7b
formatting and comments
joemarshall Nov 18, 2023
2771537
Update ci.yml with versions
joemarshall Nov 19, 2023
38be9df
pin emscripten-requirements.txt
joemarshall Nov 19, 2023
a5ead7f
derive from BaseHTTPConnection etc.
joemarshall Nov 20, 2023
9afcec1
Merge branch 'emscripten_support' of github.com:joemarshall/urllib3 i…
joemarshall Nov 20, 2023
8978367
make posargs override extra args
joemarshall Nov 20, 2023
4321c9d
fixed missing length_remaining
joemarshall Nov 20, 2023
b5aa6e5
fix response exceptions etc.
joemarshall Nov 21, 2023
e34930c
Merge branch 'main' of github.com:urllib3/urllib3 into emscripten_sup…
joemarshall Nov 22, 2023
35994ad
fixes for requests
joemarshall Nov 22, 2023
f62e0a4
mypy and lint
joemarshall Nov 22, 2023
62ad859
added coverage support to emscripten
joemarshall Nov 22, 2023
30b3063
mypy fixes
joemarshall Nov 23, 2023
ea67316
coverage updates
joemarshall Nov 24, 2023
64d4ab6
fixes to firefox coverage checking
joemarshall Nov 24, 2023
bc8c096
fixes for tests
joemarshall Nov 24, 2023
9de656b
test fixes
joemarshall Nov 24, 2023
76d39b5
Merge branch 'main' of github.com:urllib3/urllib3 into emscripten_sup…
joemarshall Nov 24, 2023
68ff2d0
test fixes
joemarshall Nov 24, 2023
1af9fdd
test bugfix
joemarshall Nov 24, 2023
19964c2
mark defensive tests
joemarshall Nov 24, 2023
985d8ca
fix tests
joemarshall Nov 24, 2023
0984732
Tests
joemarshall Nov 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .eslintrc.yml
@@ -0,0 +1,7 @@
env:
es2020 : true
worker: true
rules: {}
extends:
- eslint:recommended

10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -81,6 +81,10 @@ jobs:
os: ubuntu-20.04 # CPython 3.9.2 is not available for ubuntu-22.04.
experimental: false
nox-session: test-3.9
- python-version: "3.11"
joemarshall marked this conversation as resolved.
Show resolved Hide resolved
os: ubuntu-latest
nox-session: emscripten
experimental: true
exclude:
# Ubuntu 22.04 comes with OpenSSL 3.0, so only CPython 3.9+ is compatible with it
# https://github.com/python/cpython/issues/83001
Expand All @@ -104,6 +108,12 @@ jobs:
- name: "Install dependencies"
run: python -m pip install --upgrade pip setuptools nox

- name: "Install Chrome"
uses: browser-actions/setup-chrome@11cef13cde73820422f9263a707fb8029808e191 # v1.3.0
if: ${{ matrix.nox-session == 'emscripten' }}
- name: "Install Firefox"
uses: browser-actions/setup-firefox@29a706787c6fb2196f091563261e1273bf379ead # v1.4.0
if: ${{ matrix.nox-session == 'emscripten' }}
- name: "Run tests"
# If no explicit NOX_SESSION is set, run the default tests for the chosen Python version
run: nox -s ${NOX_SESSION:-test-$PYTHON_VERSION} --error-on-missing-interpreters
Expand Down
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Expand Up @@ -21,3 +21,14 @@ repos:
hooks:
- id: flake8
additional_dependencies: [flake8-2020]

- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.1.0"
hooks:
- id: prettier
types_or: [javascript]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.53.0
hooks:
- id: eslint
args: ["--fix"]
1 change: 1 addition & 0 deletions changelog/2951.feature.rst
@@ -0,0 +1 @@
Added support for Emscripten, including streaming support in cross-origin isolated browser environments where threading is enabled.
1 change: 1 addition & 0 deletions dummyserver/handlers.py
Expand Up @@ -247,6 +247,7 @@ def echo(self, request: httputil.HTTPServerRequest) -> Response:

def echo_json(self, request: httputil.HTTPServerRequest) -> Response:
"Echo back the JSON"
print("ECHO JSON:", request.body)
return Response(json=request.body, headers=list(request.headers.items()))

def echo_uri(self, request: httputil.HTTPServerRequest) -> Response:
Expand Down
3 changes: 3 additions & 0 deletions emscripten-requirements.txt
@@ -0,0 +1,3 @@
pytest-pyodide==0.54.0
pyodide-build==0.24.1
webdriver-manager==4.0.1
106 changes: 106 additions & 0 deletions noxfile.py
Expand Up @@ -3,6 +3,8 @@
import os
import shutil
import sys
import typing
from pathlib import Path

import nox

Expand All @@ -14,6 +16,7 @@ def tests_impl(
# https://github.com/python-hyper/h2/issues/1236
byte_string_comparisons: bool = False,
integration: bool = False,
pytest_extra_args: list[str] = [],
) -> None:
# Install deps and the package itself.
session.install("-r", "dev-requirements.txt")
Expand Down Expand Up @@ -53,6 +56,7 @@ def tests_impl(
"--durations=10",
"--strict-config",
"--strict-markers",
*pytest_extra_args,
pquentin marked this conversation as resolved.
Show resolved Hide resolved
*(session.posargs or ("test/",)),
env={"PYTHONWARNINGS": "always::DeprecationWarning"},
)
Expand Down Expand Up @@ -152,6 +156,108 @@ def lint(session: nox.Session) -> None:
mypy(session)


# TODO: node support is not tested yet - it should work if you require('xmlhttprequest') before
# loading pyodide, but there is currently no nice way to do this with pytest-pyodide
# because you can't override the test runner properties easily - see
# https://github.com/pyodide/pytest-pyodide/issues/118 for more
@nox.session(python="3.11")
joemarshall marked this conversation as resolved.
Show resolved Hide resolved
@nox.parametrize("runner", ["firefox", "chrome"])
def emscripten(session: nox.Session, runner: str) -> None:
"""Test on Emscripten with Pyodide & Chrome / Firefox"""
session.install("-r", "emscripten-requirements.txt")
# build wheel into dist folder
session.run("python", "-m", "build")
# make sure we have a dist dir for pyodide
dist_dir = None
if "PYODIDE_ROOT" in os.environ:
# we have a pyodide build tree checked out
# use the dist directory from that
dist_dir = Path(os.environ["PYODIDE_ROOT"]) / "dist"
else:
# we don't have a build tree, get one
# that matches the version of pyodide build
pyodide_version = typing.cast(
str,
session.run(
"python",
"-c",
"import pyodide_build;print(pyodide_build.__version__)",
silent=True,
),
).strip()

pyodide_artifacts_path = Path(session.cache_dir) / f"pyodide-{pyodide_version}"
if not pyodide_artifacts_path.exists():
print("Fetching pyodide build artifacts")
session.run(
"wget",
f"https://github.com/pyodide/pyodide/releases/download/{pyodide_version}/pyodide-{pyodide_version}.tar.bz2",
"-O",
f"{pyodide_artifacts_path}.tar.bz2",
)
pyodide_artifacts_path.mkdir(parents=True)
session.run(
"tar",
"-xjf",
f"{pyodide_artifacts_path}.tar.bz2",
"-C",
str(pyodide_artifacts_path),
"--strip-components",
"1",
)

dist_dir = pyodide_artifacts_path
assert dist_dir is not None
assert dist_dir.exists()
if runner == "chrome":
# install chrome webdriver and add it to path
driver = typing.cast(
str,
session.run(
"python",
"-c",
"from webdriver_manager.chrome import ChromeDriverManager;print(ChromeDriverManager().install())",
silent=True,
),
).strip()
session.env["PATH"] = f"{Path(driver).parent}:{session.env['PATH']}"

tests_impl(
session,
pytest_extra_args=[
"--rt",
"chrome-no-host",
"--dist-dir",
str(dist_dir),
"test",
],
)
joemarshall marked this conversation as resolved.
Show resolved Hide resolved
elif runner == "firefox":
driver = typing.cast(
str,
session.run(
"python",
"-c",
"from webdriver_manager.firefox import GeckoDriverManager;print(GeckoDriverManager().install())",
silent=True,
),
).strip()
session.env["PATH"] = f"{Path(driver).parent}:{session.env['PATH']}"

tests_impl(
session,
pytest_extra_args=[
"--rt",
"firefox-no-host",
"--dist-dir",
str(dist_dir),
"test",
],
)
else:
raise ValueError(f"Unknown runnner: {runner}")


@nox.session(python="3.12")
def mypy(session: nox.Session) -> None:
"""Run mypy."""
Expand Down
7 changes: 7 additions & 0 deletions src/urllib3/__init__.py
Expand Up @@ -6,6 +6,7 @@

# Set default logging handler to avoid "No handler found" warnings.
import logging
import sys
import typing
import warnings
from logging import NullHandler
Expand Down Expand Up @@ -202,3 +203,9 @@ def request(
timeout=timeout,
json=json,
)


if sys.platform == "emscripten":
from .contrib.emscripten import inject_into_urllib3 # noqa: 401

inject_into_urllib3()
Copy link
Member

Choose a reason for hiding this comment

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

Have we considered not doing this by default?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah that seems correct, we can document that you need to call this for Emscripten support.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Personally I really think it makes more sense to make it work out of the box on emscripten. Otherwise you end up with the situation that someone downloads a library which depends on requests or urllib3 and it inexplicably just doesn't work at all, and the only way they can make it work is to know that they need to read the dependency list, find urllib3 on it, and then read the urllib3 documentation to find that while urllib3 does support emscripten, you need to call a special function before urllib3 works on emscripten, and without it you just get a range of obscure not implemented errors inside socket We're already seeing similar with the current pyodide-http monkeypatch module, where you end up putting in code downstream for emscripten support that basically just looks for sys.platform and calls pyodide-http to monkeypatch everything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One possible thing to do would be to add a check for whether sockets are implemented into this code just in case something changes in the range of environments targeted by emscripten. Realistically though, sockets support is really unlikely in browser webasembly, so it is likely we'll remain in the situation where only http based protocols are available for at least the foreseeable future.

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a strong opinion, just thought it should be considered.

What are the arguments in favor of opting in? Explicit is better than implicit, mostly. Having urllib3 work magically could give the impression that sockets are supported and set unrealistic expectations. More hypothetically, having an explicit API gives us more options.

Your arguments against opting in do make sense though!

Copy link
Member

Choose a reason for hiding this comment

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

We can stick to this for now, I want to poke around for a bit once this is merged too.

4 changes: 3 additions & 1 deletion src/urllib3/connectionpool.py
Expand Up @@ -543,6 +543,8 @@ def _make_request(
response._connection = response_conn # type: ignore[attr-defined]
response._pool = self # type: ignore[attr-defined]

# emscripten connection doesn't have _http_vsn_str
http_version = getattr(conn, "_http_vsn_str", "HTTP/?")
log.debug(
'%s://%s:%s "%s %s %s" %s %s',
self.scheme,
Expand All @@ -551,7 +553,7 @@ def _make_request(
method,
url,
# HTTP version
conn._http_vsn_str, # type: ignore[attr-defined]
http_version,
response.status,
response.length_remaining, # type: ignore[attr-defined]
)
Expand Down
16 changes: 16 additions & 0 deletions src/urllib3/contrib/emscripten/__init__.py
@@ -0,0 +1,16 @@
from __future__ import annotations

import urllib3.connection

from ...connectionpool import HTTPConnectionPool, HTTPSConnectionPool
from .connection import EmscriptenHTTPConnection, EmscriptenHTTPSConnection


def inject_into_urllib3() -> None:
# override connection classes to use emscripten specific classes
# n.b. mypy complains about the overriding of classes below
# if it isn't ignored
HTTPConnectionPool.ConnectionCls = EmscriptenHTTPConnection
HTTPSConnectionPool.ConnectionCls = EmscriptenHTTPSConnection
urllib3.connection.HTTPConnection = EmscriptenHTTPConnection # type: ignore[misc,assignment]
urllib3.connection.HTTPSConnection = EmscriptenHTTPSConnection # type: ignore[misc,assignment]