Skip to content

Commit 7a2c291

Browse files
feat: add Python 3.14 support (#250)
# Summary Adds Python 3.14 to the supported/runtime-tested versions for Plugboard and aligns dependency floors with the releases needed for working 3.14 compatibility across the project and Ray-backed tests. CI and release workflows are updated so packaging and validation run against the expanded version set, and the Ray resource integration test is updated to avoid depending on the dashboard HTTP API in CI. # Changes - **Project metadata** - Adds the Python 3.14 Trove classifier. - Keeps the existing `requires-python >=3.12,<4.0` range and updates dependency floors needed for 3.14: - `msgspec[yaml] >= 0.20` - `pydantic >= 2.13.1` - Ray optional/test dependencies moved to `ray[tune] >= 2.55.0` - **CI / release pipelines** - Extends the main lint/test/build matrix to include Python 3.14. - Moves single-version workflows that build/publish artifacts to Python 3.14: - docs - docker - PyPI publish - Updates benchmarks to run on 3.14 so perf coverage tracks the newest supported interpreter. - **Lockfile refresh** - Regenerates `uv.lock` for the new dependency constraints and trims packages that were only pulled in by `ray[default]`. - **Python 3.14 Ray compatibility** - Fixes the Python 3.14 Ray test failures caused by `ray 2.55.0` resolving against an older `pydantic.v1` compatibility layer. - Bumps the workspace Pydantic floor in both `plugboard` and `plugboard-schemas` so Ray state/dashboard imports and `ray.init()` work correctly on Python 3.14. - **Test harness compatibility** - Replaces the removed `asyncio.events.BaseDefaultEventLoopPolicy` annotation in `tests/conftest.py` with a Python-3.14-safe event loop policy fixture. - Updates `tests/integration/test_component_resources.py` to verify Ray actor creation and resource reservation using Ray core APIs instead of `ray.util.state.list_actors()`, avoiding flaky failures when the dashboard API on port `8265` is unavailable in CI. ```toml classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dependencies = [ "msgspec[yaml]>=0.20,<1", "pydantic>=2.13.1,<3", ] [project.optional-dependencies] ray = ["ray[tune]>=2.55.0,<3", "jsonschema<4.25.0", "optuna>=3.0,<5"] ``` > [!WARNING] > > Ray 2.55.0 Python 3.14 support depends on resolving to a new enough `pydantic.v1` compatibility layer. This PR raises the Pydantic floor accordingly to avoid Ray startup and state API failures on Python 3.14. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: toby-coleman <13170610+toby-coleman@users.noreply.github.com> Co-authored-by: Toby Coleman <toby@tobycoleman.com>
1 parent 2c24e5f commit 7a2c291

10 files changed

Lines changed: 177 additions & 133 deletions

File tree

.github/workflows/benchmarks.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
pull-requests: write
1616
strategy:
1717
matrix:
18-
python_version: [3.12]
18+
python_version: [3.14]
1919
steps:
2020
- name: Checkout branch
2121
uses: actions/checkout@v4
@@ -80,4 +80,3 @@ jobs:
8080
repo: context.repo.repo,
8181
body: require('fs').readFileSync('${{ env.PR_COMMENT }}').toString()
8282
});
83-

.github/workflows/docker.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ on:
1313
env:
1414
REGISTRY: ghcr.io
1515
IMAGE_NAME: ${{ github.repository }}
16-
PYTHON_VERSION: '3.12'
16+
PYTHON_VERSION: '3.14'
1717

1818
# See https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images
1919
jobs:

.github/workflows/docs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010
- completed
1111

1212
env:
13-
PYTHON_VERSION: '3.12'
13+
PYTHON_VERSION: '3.14'
1414
GOOGLE_ANALYTICS_KEY: ${{ vars.GOOGLE_ANALYTICS_KEY }}
1515

1616
jobs:

.github/workflows/lint-test.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: ubuntu-latest
2020
strategy:
2121
matrix:
22-
python_version: [3.12, 3.13]
22+
python_version: [3.12, 3.13, 3.14]
2323
steps:
2424
- name: Checkout
2525
uses: actions/checkout@v4
@@ -78,7 +78,7 @@ jobs:
7878
timeout-minutes: 8
7979
strategy:
8080
matrix:
81-
python_version: [3.12, 3.13]
81+
python_version: [3.12, 3.13, 3.14]
8282
steps:
8383
- name: Checkout
8484
uses: actions/checkout@v4
@@ -116,7 +116,7 @@ jobs:
116116
timeout-minutes: 10
117117
strategy:
118118
matrix:
119-
python_version: [3.12, 3.13]
119+
python_version: [3.12, 3.13, 3.14]
120120
steps:
121121
- name: Checkout
122122
uses: actions/checkout@v4
@@ -161,7 +161,7 @@ jobs:
161161
timeout-minutes: 8
162162
strategy:
163163
matrix:
164-
python_version: [3.12, 3.13]
164+
python_version: [3.12, 3.13, 3.14]
165165
steps:
166166
- name: Checkout
167167
uses: actions/checkout@v4
@@ -201,7 +201,7 @@ jobs:
201201
timeout-minutes: 5
202202
strategy:
203203
matrix:
204-
python_version: [3.12, 3.13]
204+
python_version: [3.12, 3.13, 3.14]
205205
steps:
206206
- name: Checkout
207207
uses: actions/checkout@v4
@@ -231,7 +231,7 @@ jobs:
231231
runs-on: ubuntu-latest
232232
strategy:
233233
matrix:
234-
python_version: [3.12, 3.13]
234+
python_version: [3.12, 3.13, 3.14]
235235
steps:
236236
- name: Checkout
237237
uses: actions/checkout@v4
@@ -277,7 +277,7 @@ jobs:
277277
runs-on: ubuntu-latest
278278
strategy:
279279
matrix:
280-
python_version: [3.12, 3.13]
280+
python_version: [3.12, 3.13, 3.14]
281281
steps:
282282
- name: Checkout
283283
uses: actions/checkout@v4

.github/workflows/pypi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
- completed
88

99
env:
10-
PYTHON_VERSION: '3.12'
10+
PYTHON_VERSION: '3.14'
1111

1212
permissions:
1313
contents: read

plugboard-schemas/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ classifiers = [
1919
]
2020
dependencies = [
2121
"msgspec[yaml]>=0.18,<1",
22-
"pydantic>=2.8,<3",
22+
"pydantic>=2.8.0,<3 ; python_version < '3.14'",
23+
"pydantic>=2.13.1,<3 ; python_version >= '3.14'",
2324
]
2425

2526
[build-system]

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ classifiers = [
1515
"Topic :: Scientific/Engineering",
1616
"Programming Language :: Python :: 3.12",
1717
"Programming Language :: Python :: 3.13",
18+
"Programming Language :: Python :: 3.14",
1819
"Typing :: Typed",
1920
]
2021
dependencies = [
@@ -24,11 +25,12 @@ dependencies = [
2425
"asyncpg>=0.29.0,<1",
2526
"fsspec>=2024.9.0",
2627
"httpx>=0.27,<1",
27-
"msgspec[yaml]>=0.18,<1",
28+
"msgspec[yaml]>=0.20,<1",
2829
"pandas>=1.0,<4",
2930
"plugboard-schemas",
3031
"pyarrow>=17.0,<24",
31-
"pydantic>=2.8,<3",
32+
"pydantic>=2.8.0,<3 ; python_version < '3.14'",
33+
"pydantic>=2.13.1,<3 ; python_version >= '3.14'",
3234
"pydantic-settings>=2.7.1,<3",
3335
"pyzmq>=26.2,<28",
3436
"rich>=13.9,<15",
@@ -49,7 +51,7 @@ llm = [
4951
]
5052
# Pinning jsonschema due to performance issues with Lark and rfc3987-syntax parser
5153
# https://github.com/python-jsonschema/jsonschema/issues/1392
52-
ray = ["ray[default,tune]>=2.47.1,<3", "jsonschema<4.25.0", "optuna>=3.0,<5"]
54+
ray = ["ray[tune]>=2.47.1,<3", "jsonschema<4.25.0", "optuna>=3.0,<5"]
5355
redis = ["redis>=7.1,<8"]
5456
websockets = ["websockets>=14.2,<17"]
5557

tests/conftest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Configuration for the test suite."""
22

33
from abc import ABC
4-
from asyncio.events import BaseDefaultEventLoopPolicy
54
import multiprocessing
65
import os
76
import typing as _t
@@ -10,7 +9,6 @@
109
import pytest
1110
import pytest_asyncio
1211
import pytest_cases
13-
import ray
1412
from that_depends import ContextScopes, container_context
1513
import uvloop
1614

@@ -23,7 +21,7 @@
2321

2422

2523
@pytest.fixture(scope="session")
26-
def event_loop_policy() -> BaseDefaultEventLoopPolicy:
24+
def event_loop_policy() -> uvloop.EventLoopPolicy:
2725
"""Set uvloop as the event loop policy for the test session."""
2826
return uvloop.EventLoopPolicy()
2927

@@ -44,7 +42,9 @@ def ray_ctx() -> _t.Iterator[None]:
4442
4543
Includes a small amount of resources to allow testing of resource-constrained components.
4644
"""
47-
ray.init(num_cpus=5, num_gpus=1, resources={"custom_hardware": 10}, include_dashboard=True)
45+
import ray
46+
47+
ray.init(num_cpus=5, num_gpus=1, resources={"custom_hardware": 10}, include_dashboard=False)
4848
yield
4949
ray.shutdown()
5050

tests/integration/test_component_resources.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import typing as _t
55

66
import pytest
7-
from ray.util.state import list_actors
7+
import ray
88

99
from plugboard.component import Component, IOController as IO
1010
from plugboard.connector import RayConnector
@@ -91,16 +91,23 @@ async def test_component_resources_in_ray_process(ray_ctx: None) -> None:
9191
initial_values={"a": [5]},
9292
)
9393
connectors = [RayConnector(spec=ConnectorSpec(source="test.b", target="test.a"))]
94+
available_resources_before = ray.available_resources()
9495

9596
process = RayProcess(components=[component], connectors=connectors)
9697

9798
async with process:
98-
actors = list_actors(detail=True)
99-
component_actor = next(a for a in actors if a.name == "test")
100-
# Verify the component actor has the correct resources
101-
assert component_actor.required_resources is not None
102-
assert component_actor.required_resources["CPU"] == 1.0
103-
assert component_actor.required_resources["memory"] == 1.0 * 1024 * 1024
99+
component_actor = ray.get_actor("test", namespace=process._namespace)
100+
assert component_actor is not None
101+
# Verify the component actor has reserved the correct resources.
102+
available_resources = ray.available_resources()
103+
cpu_reserved = available_resources_before.get("CPU", 0.0) - available_resources.get(
104+
"CPU", 0.0
105+
)
106+
memory_reserved = available_resources_before.get("memory", 0.0) - available_resources.get(
107+
"memory", 0.0
108+
)
109+
assert cpu_reserved == pytest.approx(1.0)
110+
assert memory_reserved == pytest.approx(1.0 * 1024 * 1024)
104111
await process.run()
105112

106113
assert component.b == 10

0 commit comments

Comments
 (0)