Skip to content

Commit 5e9399e

Browse files
authored
Merge pull request #12 from nolar/py314-readiness
Fix for Python 3.14; enable 3.14 in CI
2 parents 7ce51ea + d35f72e commit 5e9399e

File tree

4 files changed

+29
-11
lines changed

4 files changed

+29
-11
lines changed

.github/workflows/ci.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- uses: actions/checkout@v5
1616
- uses: actions/setup-python@v6
1717
with:
18-
python-version: "3.13"
18+
python-version: "3.14"
1919
- run: pip install --group dev --group lint -e .
2020
- run: pre-commit run --all-files
2121
- run: mypy looptime --strict
@@ -24,10 +24,10 @@ jobs:
2424
strategy:
2525
fail-fast: false
2626
matrix:
27-
python-version: [ "3.10", "3.11", "3.12" ]
27+
python-version: [ "3.10", "3.11", "3.12", "3.13" ]
2828
include:
29-
- python-version: "3.13"
30-
- python-version: "3.13"
29+
- python-version: "3.14"
30+
- python-version: "3.14"
3131
install-extras: "pytest-asyncio<1.0.0"
3232
name: Python ${{ matrix.python-version }}${{ matrix.install-extras && ', ' || '' }}${{ matrix.install-extras }}
3333
runs-on: ubuntu-24.04

.github/workflows/thorough.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- uses: actions/checkout@v5
1818
- uses: actions/setup-python@v6
1919
with:
20-
python-version: "3.13"
20+
python-version: "3.14"
2121
- run: pip install --group dev --group lint -e .
2222
- run: pre-commit run --all-files
2323
- run: mypy looptime --strict
@@ -26,7 +26,7 @@ jobs:
2626
strategy:
2727
fail-fast: false
2828
matrix:
29-
python-version: [ "3.10", "3.11", "3.12", "3.13" ]
29+
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]
3030
name: Python ${{ matrix.python-version }}
3131
runs-on: ubuntu-24.04
3232
timeout-minutes: 5

looptime/plugin.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
from __future__ import annotations
109109

110110
import asyncio
111+
import sys
111112
import warnings
112113
from typing import Any
113114

@@ -166,7 +167,11 @@ def pytest_fixture_setup(fixturedef: pytest.FixtureDef[Any], request: pytest.Fix
166167
result = yield
167168

168169
# Only do the magic if in the area of our interest & only for fixtures making the event loops.
169-
if _should_patch(fixturedef, request) and isinstance(result, asyncio.BaseEventLoop):
170+
# TODO: rewrite to simpler `if` & match-case when Python 3.10 is dropped (≈October 2026).
171+
should_patch = _should_patch(fixturedef, request)
172+
is_loop = isinstance(result, asyncio.BaseEventLoop)
173+
is_runner = False if sys.version_info < (3, 11) else isinstance(result, asyncio.Runner)
174+
if should_patch and (is_loop or is_runner):
170175

171176
# Populate the helper mapper of names-to-scopes, as used in the test hook below.
172177
if EVENT_LOOP_SCOPES not in request.session.stash:
@@ -179,7 +184,15 @@ def pytest_fixture_setup(fixturedef: pytest.FixtureDef[Any], request: pytest.Fix
179184
# NB: For the lowest "function" scope, we still cannot decide which options to use, since
180185
# we do not know yet if it will be the running loop or not — so we cannot optimize here
181186
# in order to patch-and-configure only once; we must patch here & configure+activate later.
182-
result = patchers.patch_event_loop(result, _enabled=False)
187+
if isinstance(result, asyncio.BaseEventLoop):
188+
patchers.patch_event_loop(result, _enabled=False)
189+
elif sys.version_info >= (3, 11) and isinstance(result, asyncio.Runner):
190+
# Available only in python>=3.11, but mandatory for python>=3.14.
191+
# The runner of pytest-asyncio is already entered, which means the loop is created.
192+
# Even if not created, we cannot postpone the loop creation, so we create it here.
193+
loop = result.get_loop()
194+
if isinstance(loop, asyncio.BaseEventLoop):
195+
patchers.patch_event_loop(loop, _enabled=False)
183196

184197
return result
185198

@@ -188,7 +201,8 @@ def pytest_fixture_setup(fixturedef: pytest.FixtureDef[Any], request: pytest.Fix
188201
def pytest_fixture_post_finalizer(fixturedef: pytest.FixtureDef[Any], request: pytest.FixtureRequest) -> Any:
189202
# Cleanup the helper mapper of the fixture's names-to-scopes, as used in the test-running hook.
190203
# Internal consistency check: some cases should not happen, but we do not fail if they do.
191-
if EVENT_LOOP_SCOPES in request.session.stash:
204+
should_patch = _should_patch(fixturedef, request)
205+
if should_patch and EVENT_LOOP_SCOPES in request.session.stash:
192206
event_loop_scopes: EventLoopScopes = request.session.stash[EVENT_LOOP_SCOPES]
193207
if fixturedef.argname not in event_loop_scopes:
194208
warnings.warn(
@@ -232,8 +246,11 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Any:
232246
funcargs: dict[str, Any] = pyfuncitem.funcargs
233247
if 'event_loop_policy' in funcargs: # pytest-asyncio>=1.0.0
234248
# This can be ANY event loop of ANY declared scope of pytest-asyncio.
235-
policy: asyncio.AbstractEventLoopPolicy = funcargs['event_loop_policy']
236-
running_loop = policy.get_event_loop()
249+
policy = funcargs['event_loop_policy']
250+
try:
251+
running_loop = policy.get_event_loop()
252+
except RuntimeError: # a sync test with no loop set? not our business!
253+
return (yield)
237254
elif 'event_loop' in funcargs: # pytest-asyncio<1.0.0
238255
# The hook itself has NO "running" loop — because it is sync, not async.
239256
running_loop = funcargs['event_loop']

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ classifiers = [
2222
"Programming Language :: Python :: 3.11",
2323
"Programming Language :: Python :: 3.12",
2424
"Programming Language :: Python :: 3.13",
25+
"Programming Language :: Python :: 3.14",
2526
"Programming Language :: Python :: 3 :: Only",
2627
"Programming Language :: Python :: Implementation :: CPython",
2728
"Programming Language :: Python :: Implementation :: PyPy",

0 commit comments

Comments
 (0)