From ba644ff789c67bb8bcf40c180b52d85df8d13ac8 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Tue, 20 May 2025 18:48:06 +0100 Subject: [PATCH 1/4] Allow sending cookies on XMLHttpRequest.send() with (optional) withCredentials flag --- pyodide_http/__init__.py | 35 +++++++++++++++++++++++++++++++++++ pyodide_http/_core.py | 3 +++ 2 files changed, 38 insertions(+) diff --git a/pyodide_http/__init__.py b/pyodide_http/__init__.py index 6d0ad8b..308836d 100644 --- a/pyodide_http/__init__.py +++ b/pyodide_http/__init__.py @@ -5,9 +5,44 @@ except ImportError: _SHOULD_PATCH = False +from contextlib import ContextDecorator +from dataclasses import dataclass + + __version__ = "0.2.2" +@dataclass +class Options: + with_credentials: bool = False + + +_options = Options() + + +def set_with_credentials_option(value: bool): + global _options + _options.with_credentials = value + + +class option_context(ContextDecorator): + def __init__(self, with_credentials=False): + self._with_credentials = with_credentials + self._default_options = None + + def __enter__(self): + global _options + self._default_options = _options + + _options = Options() + _options.with_credentials = self._with_credentials + + def __exit__(self, *_): + if self._default_options is not None: + global _options + _options = self._default_options + + def patch_requests(continue_on_import_error: bool = False): if not _SHOULD_PATCH: return diff --git a/pyodide_http/_core.py b/pyodide_http/_core.py index d957be4..22bd8d4 100644 --- a/pyodide_http/_core.py +++ b/pyodide_http/_core.py @@ -4,6 +4,8 @@ from email.parser import Parser from pyodide.ffi import to_js +from . import _options + # need to import streaming here so that the web-worker is setup from ._streaming import send_streaming_request @@ -123,6 +125,7 @@ def send(request: Request, stream: bool = False) -> Response: if hasattr(body, 'read'): body = body.read() + xhr.withCredentials = _options.with_credentials xhr.send(to_js(body)) headers = dict(Parser().parsestr(xhr.getAllResponseHeaders())) From 78fdc9becf686961c13cfc7d974e4b5b37bf88aa Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Tue, 20 May 2025 18:49:26 +0100 Subject: [PATCH 2/4] Fix test suite, test credentials option --- setup_test_env.sh | 24 +++++++++-------- tests/pyodide_worker.js | 4 +-- tests/test_non_streaming.py | 52 ++++++++++++++++++++++++++++++++++--- tests/test_streaming.py | 6 ++++- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/setup_test_env.sh b/setup_test_env.sh index 18ef13e..c025e8c 100644 --- a/setup_test_env.sh +++ b/setup_test_env.sh @@ -4,18 +4,18 @@ set -e # install chrome -wget -nc https://dl-ssl.google.com/linux/linux_signing_key.pub -cat linux_signing_key.pub | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/linux_signing_key.gpg >/dev/null -sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/chrome.list' -sudo apt update -sudo apt install google-chrome-stable +wget -nc https://dl-ssl.google.com/linux/linux_signing_key.pub +cat linux_signing_key.pub | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/linux_signing_key.gpg >/dev/null +sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/chrome.list' +sudo apt update +sudo apt install google-chrome-stable # pyodide build and test -pip install pytest -pip install pyodide-build pytest-pyodide +pip install pytest +pip install pyodide-build pytest-pyodide # install chromedriver stuff for selenium to control chrome -pip install selenium webdriver-manager +pip install selenium webdriver-manager pip install chromedriver-binary-auto # make sure chromedriver is on path export PATH=$PATH:`chromedriver-path` @@ -23,6 +23,8 @@ export PATH=$PATH:`chromedriver-path` # run the tests cd tests # get pyodide to tests/pyodide -wget https://github.com/pyodide/pyodide/releases/download/0.21.0/pyodide-build-0.21.0.tar.bz2 -tar xjf pyodide-build-0.21.0.tar.bz2 -pytest . --dist-dir ./pyodide --rt chrome -v \ No newline at end of file +wget https://github.com/pyodide/pyodide/releases/download/0.25.1/pyodide-0.25.1.tar.bz2 +tar xjf pyodide-0.25.1.tar.bz2 +cp "$(python3 -c 'import os.path; import pytest_pyodide; print(os.path.dirname(pytest_pyodide.__file__))')/_templates/test.html" pyodide/ +cp "$(python3 -c 'import os.path; import pytest_pyodide; print(os.path.dirname(pytest_pyodide.__file__))')/_templates/webworker_dev.js" pyodide/ +pytest . --dist-dir ./pyodide --rt chrome -v diff --git a/tests/pyodide_worker.js b/tests/pyodide_worker.js index 29a1f4a..ffdecaa 100644 --- a/tests/pyodide_worker.js +++ b/tests/pyodide_worker.js @@ -1,4 +1,4 @@ -importScripts("https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js"); +importScripts("https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.js"); onmessage = async function (e) { try { @@ -25,4 +25,4 @@ onmessage = async function (e) { // if you prefer onerror events // setTimeout(() => { throw err; }); } -}; \ No newline at end of file +}; diff --git a/tests/test_non_streaming.py b/tests/test_non_streaming.py index 0d6bf04..aa35121 100644 --- a/tests/test_non_streaming.py +++ b/tests/test_non_streaming.py @@ -1,8 +1,9 @@ from pathlib import Path +import os.path import glob from pytest_pyodide import run_in_pyodide, spawn_web_server -from pytest import fixture +from pytest import fixture, fail @fixture(scope="module") @@ -43,6 +44,9 @@ def _install_package(selenium, base_url): import requests """ ) + break + else: + fail(f"no pre-built *.whl found in {os.path.relpath(wheel_folder)}") def test_install_package(selenium_standalone, web_server_base): @@ -56,15 +60,52 @@ def test_requests_get(selenium_standalone, dist_dir, web_server_base): def test_fn(selenium_standalone, base_url): import requests + import pyodide_http._requests + assert pyodide_http._requests._IS_PATCHED + + import pyodide_http as ph + print("get:", base_url) - url = f"{base_url}/yt-4.0.4-cp310-cp310-emscripten_3_1_14_wasm32.whl" - resp = requests.get(url) + url = f"{base_url}/yt-4.1.4-cp311-cp311-emscripten_3_1_46_wasm32.whl" + + # The test web server sets "Access-Control-Allow-Origin: *" which disallows sending credentials. + # Credentials are explicitly disabled, although that's the default, to exercise the option code. + with ph.option_context(with_credentials=False): + resp = requests.get(url) + data = resp.content assert resp.request.url == url return len(data) - assert test_fn(selenium_standalone, f"{web_server_base}{dist_dir}/") == 11373926 + assert test_fn(selenium_standalone, f"{web_server_base}{dist_dir}/") == 78336150 + + +def test_urllib_get(selenium_standalone, dist_dir, web_server_base): + _install_package(selenium_standalone, web_server_base) + + @run_in_pyodide + def test_fn(selenium_standalone, base_url): + import urllib.request + + import pyodide_http._urllib + assert pyodide_http._urllib._IS_PATCHED + + import pyodide_http as ph + + # The test web server sets "Access-Control-Allow-Origin: *" which disallows sending credentials. + # Credentials are explicitly disabled, although that's the default, to exercise the option code. + ph.set_with_credentials_option(False) + + print("get:", base_url) + url = f"{base_url}/yt-4.1.4-cp311-cp311-emscripten_3_1_46_wasm32.whl" + with urllib.request.urlopen(url) as resp: + data = resp.read() + assert resp.url == url + + return len(data) + + assert test_fn(selenium_standalone, f"{web_server_base}{dist_dir}/") == 78336150 def test_requests_404(selenium_standalone, dist_dir, web_server_base): @@ -74,6 +115,9 @@ def test_requests_404(selenium_standalone, dist_dir, web_server_base): def test_fn(selenium_standalone, base_url): import requests + import pyodide_http._requests + assert pyodide_http._requests._IS_PATCHED + print("get:", base_url) resp = requests.get(f"{base_url}/surely_this_file_does_not_exist.hopefully.") response = resp.status_code diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 193611b..be41df4 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -9,10 +9,11 @@ import socketserver from pathlib import Path +import os.path import glob import pytest_pyodide -from pytest import fixture +from pytest import fixture, fail from pytest_pyodide import run_in_pyodide @@ -160,6 +161,9 @@ def _install_package(selenium, base_url): import requests """ ) + break + else: + fail(f"no pre-built *.whl found in {os.path.relpath(wheel_folder)}") def get_install_package_code(base_url): From dae0b7558526109db8f96a6ad2a58cadc84e3260 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Tue, 20 May 2025 18:56:50 +0100 Subject: [PATCH 3/4] Increment version --- pyodide_http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyodide_http/__init__.py b/pyodide_http/__init__.py index 308836d..1d19c45 100644 --- a/pyodide_http/__init__.py +++ b/pyodide_http/__init__.py @@ -9,7 +9,7 @@ from dataclasses import dataclass -__version__ = "0.2.2" +__version__ = "0.2.3" @dataclass From 8b02809fa572ff32f28410246ee48fd66a1066ac Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Tue, 20 May 2025 19:06:31 +0100 Subject: [PATCH 4/4] Add tests for setting the credentials option --- tests/test_non_streaming.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_non_streaming.py b/tests/test_non_streaming.py index aa35121..fd38716 100644 --- a/tests/test_non_streaming.py +++ b/tests/test_non_streaming.py @@ -53,6 +53,37 @@ def test_install_package(selenium_standalone, web_server_base): _install_package(selenium_standalone, web_server_base) +def test_credentials_context_manager(selenium_standalone, dist_dir, web_server_base): + _install_package(selenium_standalone, web_server_base) + + @run_in_pyodide + def test_fn(selenium_standalone, base_url): + import pyodide_http as ph + + assert not ph._options.with_credentials + + with ph.option_context(with_credentials=True): + assert ph._options.with_credentials + + assert not ph._options.with_credentials + + +def test_credentials_option(selenium_standalone, dist_dir, web_server_base): + _install_package(selenium_standalone, web_server_base) + + @run_in_pyodide + def test_fn(selenium_standalone, base_url): + import pyodide_http as ph + + assert not ph._options.with_credentials + + ph.set_with_credentials_option(True) + assert ph._options.with_credentials + + ph.set_with_credentials_option(False) + assert not ph._options.with_credentials + + def test_requests_get(selenium_standalone, dist_dir, web_server_base): _install_package(selenium_standalone, web_server_base)