Skip to content
Open
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
4 changes: 3 additions & 1 deletion packages/cli/src/pywrangler/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@


class PythonCompatVersion(NamedTuple):
version: Literal["3.12", "3.13"]
version: Literal["3.12", "3.13", "3.14"]
compat_flag: str
compat_date: datetime | None
experimental: bool = False


PYTHON_COMPAT_VERSIONS = [
PythonCompatVersion("3.14", "python_workers_20260610", None, experimental=True),
PythonCompatVersion(
"3.13", "python_workers_20250116", datetime.strptime("2025-09-29", "%Y-%m-%d")
),
Expand Down
12 changes: 10 additions & 2 deletions packages/cli/src/pywrangler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def get_pywrangler_config() -> dict:


MIN_UV_VERSION = (0, 8, 10)
MIN_WRANGLER_VERSION = (4, 42, 1)
MIN_WRANGLER_VERSION = (4, 109, 0)


def check_uv_version() -> None:
Expand Down Expand Up @@ -344,7 +344,7 @@ def _parse_wrangler_config() -> WranglerConfig:


@cache
def get_python_version() -> Literal["3.12", "3.13"]:
def get_python_version() -> Literal["3.12", "3.13", "3.14"]:
"""
Determine Python version from wrangler configuration.

Expand Down Expand Up @@ -382,6 +382,10 @@ def get_python_version() -> Literal["3.12", "3.13"]:
)

for py_version in sorted_versions:
# Skip experimental versions unless the experimental compat flag is enabled
if "experimental" not in compat_flags and py_version.experimental:
continue

# Check if the specific compat flag is present
if py_version.compat_flag in compat_flags:
return py_version.version
Expand All @@ -400,6 +404,8 @@ def get_uv_pyodide_interp_name() -> str:
v = "3.12.7"
case "3.13":
v = "3.13.2"
case "3.14":
v = "3.14.2"
return f"cpython-{v}-emscripten-wasm32-musl"


Expand All @@ -417,6 +423,8 @@ def get_pyodide_index() -> str:
v = "0.27.7"
case "3.13":
v = "0.28.3"
case "3.14":
v = "314.0.2"
return "https://index.pyodide.org/" + v


Expand Down
19 changes: 15 additions & 4 deletions packages/cli/tests/bindings-test/src/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,19 @@ def _run_suite(self, suite_name):
)

collector = ResultCollector()
pytest.main(
["--pyargs", module, "-p", "no:cacheprovider"],
plugins=[collector, EnvPlugin(self.env)],
)
# pytest-asyncio drives each test through asyncio.Runner, which calls
# asyncio.new_event_loop(). In Pyodide that constructs a WebLoop whose
# __init__ calls asyncio._set_running_loop(self) and is never restored on
# close(), so after pytest.main() the running loop points at an abandoned
# WebLoop. Save and restore it so the next request's fetch coroutine runs
# on the real workerd-driven loop instead of hanging on a dead one.
# TODO: fix this behavior in Pyodide
saved_loop = asyncio.events._get_running_loop()
try:
pytest.main(
["--pyargs", module, "-p", "no:cacheprovider"],
plugins=[collector, EnvPlugin(self.env)],
)
finally:
asyncio.events._set_running_loop(saved_loop)
return Response.json(collector.results)
50 changes: 44 additions & 6 deletions packages/cli/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
# Compat dates used to parametrize workerd and bindings integration tests.
# Each date exercises a different Python version inside the worker runtime:
# - "2025-09-01" -> Python 3.12 (before the 2025-09-29 cutover)
# - "2026-01-01" -> Python 3.13 (after the 2025-09-29 cutover)
# TODO: use compat flag instead of compat date
from dataclasses import dataclass, field
from pathlib import Path

COMPAT_DATES: list[str] = ["2025-09-01", "2026-01-01"]

@dataclass(frozen=True)
class CompatConfig:
compat_date: str
python_version: str
extra_compat_flags: list[str] = field(default_factory=list)


COMPAT_CONFIGS: list[CompatConfig] = [
CompatConfig(
compat_date="2025-09-01",
python_version="3.12",
extra_compat_flags=[
"enable_python_external_sdk",
"python_process_pth_files",
"python_request_headers_preserve_commas",
],
),
CompatConfig(
compat_date="2026-01-01",
python_version="3.13",
extra_compat_flags=[
"enable_python_external_sdk",
"python_process_pth_files",
"python_request_headers_preserve_commas",
],
),
CompatConfig(
compat_date="2026-07-01",
python_version="3.14",
# TODO: remove these when 3.14 is stable, and enabled by date
extra_compat_flags=["python_workers_20260610", "experimental"],
),
]


def replace_compat_date(file: Path, compat_date: str) -> None:
file.write_text(file.read_text().replace("%COMPAT_DATE", compat_date))


def inject_compat_flags(file: Path, extra_flags: list[str]) -> None:
if not extra_flags:
return
content = file.read_text()
for flag in extra_flags:
content = content.replace('"python_workers"', f'"python_workers", "{flag}"')
file.write_text(content)
31 changes: 20 additions & 11 deletions packages/cli/tests/test_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@

import pytest
import requests
from conftest import COMPAT_DATES, replace_compat_date
from conftest import (
COMPAT_CONFIGS,
CompatConfig,
inject_compat_flags,
replace_compat_date,
)

TEST_DIR: Path = Path(__file__).parent
BINDINGS_TEST_DIR: Path = TEST_DIR / "bindings-test"
Expand Down Expand Up @@ -74,25 +79,33 @@ def _wait_for_ready(process: subprocess.Popen[str], base_url: str) -> None:
pytest.fail(f"pywrangler dev did not become ready within {DEV_STARTUP_TIMEOUT}s")


@pytest.fixture(scope="module", params=COMPAT_DATES)
def compat_date(request: pytest.FixtureRequest) -> str:
@pytest.fixture(
scope="module",
params=COMPAT_CONFIGS,
ids=[c.python_version for c in COMPAT_CONFIGS],
)
def compat_config(request: pytest.FixtureRequest) -> CompatConfig:
return request.param


@pytest.fixture(scope="module")
def dev_server(
tmp_path_factory: pytest.TempPathFactory, compat_date: str
tmp_path_factory: pytest.TempPathFactory, compat_config: CompatConfig
) -> Generator[str]:
"""Start a pywrangler dev server on a free port and yield its base URL."""
tmp_path = tmp_path_factory.mktemp("bindings_test")
target = tmp_path / "bindings-test"
shutil.copytree(BINDINGS_TEST_DIR, target)
env = os.environ | {"_PYODIDE_EXTRA_MOUNTS": str(tmp_path)}

replace_compat_date(target / "wrangler.jsonc", compat_date)
wrangler_jsonc = target / "wrangler.jsonc"
replace_compat_date(wrangler_jsonc, compat_config.compat_date)
inject_compat_flags(wrangler_jsonc, compat_config.extra_compat_flags)

pywrangler_cmd = ["uv", "run", "--no-project", "--with", WORKERS_PY, "pywrangler"]

subprocess.run(
["uv", "run", "--with", WORKERS_PY, "pywrangler", "sync"],
[*pywrangler_cmd, "sync"],
cwd=target,
check=True,
env=env,
Expand All @@ -105,11 +118,7 @@ def dev_server(

process = subprocess.Popen(
[
"uv",
"run",
"--with",
WORKERS_PY,
"pywrangler",
*pywrangler_cmd,
"dev",
"--port",
str(port),
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ def test_check_wrangler_version_sufficient(mock_run_command):
# Mock successful wrangler version output
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = "wrangler 4.42.1"
mock_result.stdout = "wrangler 4.109.0"
mock_run_command.return_value = mock_result

# Should not raise an exception
Expand Down
31 changes: 26 additions & 5 deletions packages/cli/tests/test_in_workerd.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
from pathlib import Path

import pytest
from conftest import COMPAT_DATES, replace_compat_date
from conftest import (
COMPAT_CONFIGS,
CompatConfig,
inject_compat_flags,
replace_compat_date,
)

TEST_DIR = Path(__file__).parent
WORKERD_TESTS = TEST_DIR / "workerd-test"
Expand Down Expand Up @@ -51,11 +56,22 @@ def bundle_cache_dir(tmp_path_factory):
yield tmp_path_factory.mktemp("bundle_cache")


@pytest.mark.parametrize("compat_date", COMPAT_DATES)
@pytest.mark.parametrize(
"compat_config",
COMPAT_CONFIGS,
ids=[c.python_version for c in COMPAT_CONFIGS],
)
@pytest.mark.parametrize("test_dir, wd_test_file", discover_workerd_tests())
def test_in_workerd( # noqa: PLR0913 (too-many-arguments)
tmp_path, test_dir, wd_test_file, compat_date, pytestconfig, bundle_cache_dir
tmp_path,
test_dir,
wd_test_file,
compat_config: CompatConfig,
pytestconfig,
bundle_cache_dir,
):
compat_date = compat_config.compat_date

# FIXME:
# pywrangler sync fails to install pyodide packages in unittest environment + Python 3.12 + Linux
# This is reproducible only in the unittest environment, and doesn't happen
Expand All @@ -81,9 +97,12 @@ def test_in_workerd( # noqa: PLR0913 (too-many-arguments)
disk_service_dir.mkdir(exist_ok=True)

replace_compat_date(target / "wrangler.jsonc", compat_date)
inject_compat_flags(target / "wrangler.jsonc", compat_config.extra_compat_flags)

pywrangler_cmd = ["uv", "run", "--no-project", "--with", WORKERS_PY, "pywrangler"]

subprocess.run(
["uv", "run", "--with", WORKERS_PY, "pywrangler", "sync"],
[*pywrangler_cmd, "sync"],
cwd=target,
check=True,
)
Expand All @@ -105,6 +124,8 @@ def test_in_workerd( # noqa: PLR0913 (too-many-arguments)
.replace("%COLOR", str(color).lower())
.replace("%COMPAT_DATE", compat_date)
)
inject_compat_flags(wd_config, compat_config.extra_compat_flags)

subprocess.run(
["npm", "i", "workerd"],
cwd=target,
Expand All @@ -119,7 +140,7 @@ def test_in_workerd( # noqa: PLR0913 (too-many-arguments)
".",
f"-d{DISK_SERVICE_NAME}={disk_service_dir}",
"--pyodide-bundle-disk-cache-dir",
bundle_cache_dir / compat_date,
bundle_cache_dir / compat_config.python_version,
]
subprocess.run(
[
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/tests/test_py_version_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,15 @@ def test_main_get_python_version_integration(test_dir):

version = get_python_version()
assert version == "3.12"


def test_314_compat_flag_with_experimental(test_dir):
wrangler_toml = test_dir / "wrangler.toml"
wrangler_toml.write_text("""
name = "test-worker"
compatibility_flags = ["python_workers", "python_workers_20260610", "experimental"]
compatibility_date = "2026-06-10"
""")

version = get_python_version()
assert version == "3.14"
Comment thread
ryanking13 marked this conversation as resolved.
2 changes: 1 addition & 1 deletion packages/cli/tests/workerd-test/asgi/asgi.wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const unitTests :Workerd.Config = (
( name = "SELF", service = "python-asgi" ),
],
compatibilityDate = "%COMPAT_DATE",
compatibilityFlags = ["python_workers", "enable_python_external_sdk"],
compatibilityFlags = ["python_workers"],
)
)
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ const mainWorker :Workerd.Worker = (
(name = "ABORTER", durableObjectNamespace = "AbortingDurableObject"),
],
compatibilityDate = "%COMPAT_DATE",
compatibilityFlags = ["python_workers", "service_binding_extra_handlers", "enable_python_external_sdk"],
compatibilityFlags = ["python_workers", "service_binding_extra_handlers"],
);
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ const mainWorker :Workerd.Worker = (
(name = "DO_REDUNDANT", durableObjectNamespace = "RedundantBaseDO"),
],
compatibilityDate = "%COMPAT_DATE",
compatibilityFlags = ["python_workers", "service_binding_extra_handlers", "enable_python_external_sdk"],
compatibilityFlags = ["python_workers", "service_binding_extra_handlers"],
);
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const mainWorker :Workerd.Worker = (
(name = "ns", durableObjectNamespace = "DurableObjectWebSocket"),
],
compatibilityDate = "%COMPAT_DATE",
compatibilityFlags = ["python_workers", "enable_python_external_sdk"],
compatibilityFlags = ["python_workers"],
);

const testerWorker :Workerd.Worker = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ const mainWorker :Workerd.Worker = (
(name = "ns", durableObjectNamespace = "DurableObjectExample"),
],
compatibilityDate = "%COMPAT_DATE",
compatibilityFlags = ["python_workers", "service_binding_extra_handlers", "enable_python_external_sdk"],
compatibilityFlags = ["python_workers", "service_binding_extra_handlers"],
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const python :Workerd.Worker = (
%PYTHON_MODULES
],
compatibilityDate = "%COMPAT_DATE",
compatibilityFlags = ["python_workers", "service_binding_extra_handlers", "enable_python_external_sdk", "python_process_pth_files"],
compatibilityFlags = ["python_workers", "service_binding_extra_handlers"],
);

const unitTests :Workerd.Config = (
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/tests/workerd-test/entropy-patches/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ async def noop(*args):

class Default(WorkerEntrypoint):
async def test(self):
if sys.version_info >= (3, 14):
# FIXME(soon): fix entropy patches for newer packages

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

pydantic-core issue. We need to fix this, but it should be done in a separate PR.

return

os.chdir("/session/metadata/tests")
args = [".", "-vv"]
assert pytest.main(args) == 0
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const pyWorker :Workerd.Worker = (
(name = "FOO", text = "text binding"),
],
compatibilityDate = "%COMPAT_DATE",
compatibilityFlags = ["python_workers", "service_binding_extra_handlers", "enable_python_external_sdk"],
compatibilityFlags = ["python_workers", "service_binding_extra_handlers"],
);

const jsWorker :Workerd.Worker = (
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/tests/workerd-test/sdk/sdk.wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const python :Workerd.Worker = (
),
],
compatibilityDate = "%COMPAT_DATE",
compatibilityFlags = ["python_workers", "service_binding_extra_handlers", "python_request_headers_preserve_commas", "enable_python_external_sdk"],
compatibilityFlags = ["python_workers", "service_binding_extra_handlers"],
);

const server :Workerd.Worker = (
Expand All @@ -22,7 +22,7 @@ const server :Workerd.Worker = (
%PYTHON_MODULES
],
compatibilityDate = "%COMPAT_DATE",
compatibilityFlags = ["python_workers", "python_request_headers_preserve_commas", "enable_python_external_sdk"],
compatibilityFlags = ["python_workers"],
);

const unitTests :Workerd.Config = (
Expand Down
Loading
Loading