Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 8 additions & 10 deletions packages/runtime-sdk/src/workers/utils.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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()
Expand Down
26 changes: 26 additions & 0 deletions packages/runtime-sdk/tests/test_flask.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[project]
name = "flask-tests"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["flask", "pytest"]
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "flask-tests",
"main": "src/worker.py",
"compatibility_date": "%COMPAT_DATE",
"compatibility_flags": ["python_workers"]
}
Loading