diff --git a/packages/runtime-sdk/src/workers/utils.py b/packages/runtime-sdk/src/workers/utils.py index 6b0fc410..ea4018d2 100644 --- a/packages/runtime-sdk/src/workers/utils.py +++ b/packages/runtime-sdk/src/workers/utils.py @@ -1,4 +1,5 @@ -from collections.abc import Iterator, Sequence +import http.client +from collections.abc import Iterator, Mapping, Sequence from contextlib import contextmanager from typing import Any @@ -201,22 +202,19 @@ def _to_js_headers(headers): raise TypeError("Received unexpected type for headers argument") +class HTTPMessageMapping(http.client.HTTPMessage, Mapping): + pass + + def _js_headers_to_http_message( js_headers: dict[str, str], ): - # `http.client` is imported here because it costs a lot of CPU time when imported at the - # top-level. At least it does when we do so in our validator tests, doesn't seem to cause - # trouble in production. So as a workaround we do the import here. - # - # TODO(later): when dedicated snapshots are default we can move this import to the top-level. - import http.client - # Newer Pyodide versions already expose headers as an http.client.HTTPMessage, # in which case there is nothing to convert. - if isinstance(js_headers, http.client.HTTPMessage): + if isinstance(js_headers, HTTPMessageMapping): return js_headers - result = http.client.HTTPMessage() + result = HTTPMessageMapping() if not get_compat_flag("python_request_headers_preserve_commas"): for key, val in js_headers: result[key] = val.strip() diff --git a/packages/runtime-sdk/tests/test_flask.py b/packages/runtime-sdk/tests/test_flask.py new file mode 100644 index 00000000..ce8cb3ef --- /dev/null +++ b/packages/runtime-sdk/tests/test_flask.py @@ -0,0 +1,26 @@ +"""Tests for Flask running against a live pywrangler dev server.""" + +from pathlib import Path + +import pytest +from conftest import COMPAT_CONFIGS, CompatConfig, register_in_worker_suites + +WEB_FRAMEWORKS_DIR = Path(__file__).parent / "web-frameworks-test" / "flask-tests" +WEB_FRAMEWORKS_SRC_DIR = WEB_FRAMEWORKS_DIR / "src" + + +@pytest.fixture(scope="module") +def worker_project_dir() -> Path: + return WEB_FRAMEWORKS_DIR + + +@pytest.fixture( + scope="module", + params=COMPAT_CONFIGS, + ids=[config.python_version for config in COMPAT_CONFIGS], +) +def compat_config(request: pytest.FixtureRequest) -> CompatConfig: + return request.param + + +register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR) diff --git a/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/pyproject.toml b/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/pyproject.toml new file mode 100644 index 00000000..6f0188c4 --- /dev/null +++ b/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "flask-tests" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = ["flask", "pytest"] diff --git a/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/src/test_response.py b/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/src/test_response.py new file mode 100644 index 00000000..30cb93b7 --- /dev/null +++ b/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/src/test_response.py @@ -0,0 +1,13 @@ +from flask import Response as FlaskResponse + +from workers import FetchResponse, Response + + +def test_fetch_response_headers(): + fetch_response = Response(headers={"X-Workers-Test": "value"}) + + assert isinstance(fetch_response, FetchResponse) + + flask_response = FlaskResponse(headers=fetch_response.headers) + + assert flask_response.headers["X-Workers-Test"] == "value" diff --git a/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/src/worker.py b/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/src/worker.py new file mode 100644 index 00000000..18d843e8 --- /dev/null +++ b/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/src/worker.py @@ -0,0 +1,89 @@ +import importlib.util +from urllib.parse import urlparse + +import pytest +from flask import Flask + +from workers import Response, WorkerEntrypoint, wsgi + +app = Flask(__name__) + + +@app.get("/health") +def health(): + return {"ok": True} + + +class ResultCollector: + def __init__(self): + self.results = {} + + @staticmethod + def _key(item): + name = item.name + return name[len("test_") :] if name.startswith("test_") else name + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_makereport(self, item, call): + outcome = yield + report = outcome.get_result() + key = self._key(item) + + if report.when == "call": + if report.passed: + self.results[key] = {"status": "passed"} + elif report.skipped: + self.results[key] = { + "status": "skipped", + "reason": str(report.longrepr), + } + elif report.failed: + excinfo = call.excinfo + if excinfo is not None and excinfo.errisinstance(AssertionError): + self.results[key] = { + "status": "failed", + "error": str(excinfo.value), + } + else: + self.results[key] = { + "status": "error", + "error": f"{excinfo.typename}: {excinfo.value}" + if excinfo is not None + else "unknown error", + "traceback": report.longreprtext, + } + elif report.when in ("setup", "teardown") and report.skipped: + self.results[key] = { + "status": "skipped", + "reason": str(report.longrepr), + } + elif report.when in ("setup", "teardown") and report.failed: + self.results[key] = { + "status": "error", + "error": report.longreprtext, + "traceback": report.longreprtext, + } + + +class Default(WorkerEntrypoint): + async def fetch(self, request): + path = urlparse(request.url).path + if path.startswith("/run-tests/"): + return self._run_suite(path[len("/run-tests/") :]) + return await wsgi.fetch(app, request, self.env, self.ctx) + + @staticmethod + def _run_suite(suite_name): + module = f"test_{suite_name}" + if importlib.util.find_spec(module) is None: + return Response.json( + {"error": f"Unknown suite '{suite_name}' (no module '{module}')"}, + status=404, + ) + + collector = ResultCollector() + pytest.main( + ["--pyargs", module, "-p", "no:cacheprovider"], + plugins=[collector], + ) + return Response.json(collector.results) diff --git a/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/wrangler.jsonc b/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/wrangler.jsonc new file mode 100644 index 00000000..1718baac --- /dev/null +++ b/packages/runtime-sdk/tests/web-frameworks-test/flask-tests/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "flask-tests", + "main": "src/worker.py", + "compatibility_date": "%COMPAT_DATE", + "compatibility_flags": ["python_workers"] +}