Skip to content

Commit 4e824df

Browse files
authored
add test run for 3.13t (#1626)
1 parent f7fb50b commit 4e824df

16 files changed

+89
-52
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,15 @@ jobs:
6868
- '3.11'
6969
- '3.12'
7070
- '3.13'
71+
- '3.13t'
7172
- 'pypy3.9'
7273
- 'pypy3.10'
7374

7475
runs-on: ubuntu-latest
7576

77+
# TODO: get test suite stable with free-threaded python
78+
continue-on-error: ${{ endsWith(matrix.python-version, 't') }}
79+
7680
steps:
7781
- uses: actions/checkout@v4
7882

@@ -101,6 +105,11 @@ jobs:
101105
- run: uv run pytest
102106
env:
103107
HYPOTHESIS_PROFILE: slow
108+
# TODO: remove -x when test suite is more stable; we use it so that first error (hopefully) gets
109+
# reported without the interpreter crashing
110+
PYTEST_ADDOPTS: ${{ endsWith(matrix.python-version, 't') && '--parallel-threads=2 -x' || '' }}
111+
# TODO: add `gil_used = false` to the PyO3 `#[pymodule]` when test suite is ok
112+
PYTHON_GIL: ${{ endsWith(matrix.python-version, 't') && '0' || '1' }}
104113

105114
test-os:
106115
name: test on ${{ matrix.os }}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ testing = [
5252
'pytest-speed',
5353
'pytest-mock',
5454
'pytest-pretty',
55+
'pytest-run-parallel',
5556
'pytest-timeout',
5657
'python-dateutil',
5758
# numpy doesn't offer prebuilt wheels for all versions and platforms we test in CI e.g. aarch64 musllinux

src/argument_markers.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ use pyo3::types::{PyDict, PyTuple};
55

66
use crate::tools::safe_repr;
77

8-
#[pyclass(module = "pydantic_core._pydantic_core", get_all, frozen, freelist = 100)]
8+
// see https://github.com/PyO3/pyo3/issues/4894 - freelist is currently unsound with GIL disabled
9+
#[cfg_attr(
10+
not(Py_GIL_DISABLED),
11+
pyclass(module = "pydantic_core._pydantic_core", get_all, frozen, freelist = 100)
12+
)]
13+
#[cfg_attr(Py_GIL_DISABLED, pyclass(module = "pydantic_core._pydantic_core", get_all, frozen))]
914
#[derive(Debug, Clone)]
1015
pub struct ArgsKwargs {
1116
pub(crate) args: Py<PyTuple>,

tests/conftest.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from __future__ import annotations as _annotations
22

33
import functools
4+
import gc
45
import importlib.util
56
import json
67
import os
78
import re
89
from dataclasses import dataclass
910
from pathlib import Path
10-
from typing import Any, Literal
11+
from time import sleep, time
12+
from typing import Any, Callable, Literal
1113

1214
import hypothesis
1315
import pytest
@@ -160,3 +162,20 @@ def infinite_generator():
160162
while True:
161163
yield i
162164
i += 1
165+
166+
167+
def assert_gc(test: Callable[[], bool], timeout: float = 10) -> None:
168+
"""Helper to retry garbage collection until the test passes or timeout is
169+
reached.
170+
171+
This is useful on free-threading where the GC collect call finishes before
172+
all cleanup is done.
173+
"""
174+
start = now = time()
175+
while now - start < timeout:
176+
if test():
177+
return
178+
gc.collect()
179+
sleep(0.1)
180+
now = time()
181+
raise AssertionError('Timeout waiting for GC')

tests/test_docstrings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def find_examples(*_directories):
1414

1515
@pytest.mark.skipif(CodeExample is None or sys.platform not in {'linux', 'darwin'}, reason='Only on linux and macos')
1616
@pytest.mark.parametrize('example', find_examples('python/pydantic_core/core_schema.py'), ids=str)
17+
@pytest.mark.thread_unsafe # TODO investigate why pytest_examples seems to be thread unsafe here
1718
def test_docstrings(example: CodeExample, eval_example: EvalExample):
1819
eval_example.set_config(quotes='single')
1920

@@ -27,6 +28,7 @@ def test_docstrings(example: CodeExample, eval_example: EvalExample):
2728

2829
@pytest.mark.skipif(CodeExample is None or sys.platform not in {'linux', 'darwin'}, reason='Only on linux and macos')
2930
@pytest.mark.parametrize('example', find_examples('README.md'), ids=str)
31+
@pytest.mark.thread_unsafe # TODO investigate why pytest_examples seems to be thread unsafe here
3032
def test_readme(example: CodeExample, eval_example: EvalExample):
3133
eval_example.set_config(line_length=100, quotes='single')
3234
if eval_example.update_examples:

tests/test_garbage_collection.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import gc
21
import platform
32
from collections.abc import Iterable
43
from typing import Any
@@ -8,6 +7,8 @@
87

98
from pydantic_core import SchemaSerializer, SchemaValidator, core_schema
109

10+
from .conftest import assert_gc
11+
1112
GC_TEST_SCHEMA_INNER = core_schema.definitions_schema(
1213
core_schema.definition_reference_schema(schema_ref='model'),
1314
[
@@ -43,11 +44,7 @@ class MyModel(BaseModel):
4344

4445
del MyModel
4546

46-
gc.collect(0)
47-
gc.collect(1)
48-
gc.collect(2)
49-
50-
assert len(cache) == 0
47+
assert_gc(lambda: len(cache) == 0)
5148

5249

5350
@pytest.mark.xfail(
@@ -75,11 +72,7 @@ class MyModel(BaseModel):
7572

7673
del MyModel
7774

78-
gc.collect(0)
79-
gc.collect(1)
80-
gc.collect(2)
81-
82-
assert len(cache) == 0
75+
assert_gc(lambda: len(cache) == 0)
8376

8477

8578
@pytest.mark.xfail(
@@ -114,8 +107,4 @@ def __next__(self):
114107
v.validate_python({'iter': iterable})
115108
del iterable
116109

117-
gc.collect(0)
118-
gc.collect(1)
119-
gc.collect(2)
120-
121-
assert len(cache) == 0
110+
assert_gc(lambda: len(cache) == 0)

tests/test_hypothesis.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ def datetime_schema():
1919

2020

2121
@given(strategies.datetimes())
22+
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
2223
def test_datetime_datetime(datetime_schema, data):
2324
assert datetime_schema.validate_python(data) == data
2425

2526

2627
@pytest.mark.skipif(sys.platform == 'win32', reason='Can fail on windows, I guess due to 64-bit issue')
2728
@given(strategies.integers(min_value=-11_676_096_000, max_value=253_402_300_799_000))
29+
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
2830
def test_datetime_int(datetime_schema, data):
2931
try:
3032
if abs(data) > 20_000_000_000:
@@ -41,6 +43,7 @@ def test_datetime_int(datetime_schema, data):
4143

4244

4345
@given(strategies.binary())
46+
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
4447
def test_datetime_binary(datetime_schema, data):
4548
try:
4649
datetime_schema.validate_python(data)
@@ -89,6 +92,7 @@ class BranchModel(TypedDict):
8992

9093
@pytest.mark.skipif(sys.platform == 'emscripten', reason='Seems to fail sometimes on pyodide no idea why')
9194
@given(strategies.from_type(BranchModel))
95+
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
9296
def test_recursive(definition_schema, data):
9397
assert definition_schema.validate_python(data) == data
9498

@@ -108,6 +112,7 @@ def branch_models_with_cycles(draw, existing=None):
108112

109113

110114
@given(branch_models_with_cycles())
115+
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
111116
def test_definition_cycles(definition_schema, data):
112117
try:
113118
assert definition_schema.validate_python(data) == data
@@ -130,6 +135,7 @@ def test_definition_broken(definition_schema):
130135

131136

132137
@given(strategies.timedeltas())
138+
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
133139
def test_pytimedelta_as_timedelta(dt):
134140
v = SchemaValidator({'type': 'timedelta', 'gt': dt})
135141
# simplest way to check `pytimedelta_as_timedelta` is correct is to extract duration from repr of the validator
@@ -150,6 +156,7 @@ def url_validator():
150156

151157

152158
@given(strategies.text())
159+
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
153160
def test_urls_text(url_validator, text):
154161
try:
155162
url_validator.validate_python(text)
@@ -166,6 +173,7 @@ def multi_host_url_validator():
166173

167174

168175
@given(strategies.text())
176+
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
169177
def test_multi_host_urls_text(multi_host_url_validator, text):
170178
try:
171179
multi_host_url_validator.validate_python(text)
@@ -182,6 +190,7 @@ def str_serializer():
182190

183191

184192
@given(strategies.text())
193+
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
185194
def test_serialize_string(str_serializer: SchemaSerializer, data):
186195
assert str_serializer.to_python(data) == data
187196
assert json.loads(str_serializer.to_json(data)) == data

tests/validators/test_dataclasses.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import dataclasses
2-
import gc
32
import platform
43
import re
54
import sys
@@ -11,7 +10,7 @@
1110

1211
from pydantic_core import ArgsKwargs, SchemaValidator, ValidationError, core_schema
1312

14-
from ..conftest import Err, PyAndJson
13+
from ..conftest import Err, PyAndJson, assert_gc
1514

1615

1716
@pytest.mark.parametrize(
@@ -1586,12 +1585,8 @@ def _wrap_validator(cls, v, validator, info):
15861585
assert ref() is not None
15871586

15881587
del klass
1589-
gc.collect(0)
1590-
gc.collect(1)
1591-
gc.collect(2)
1592-
gc.collect()
15931588

1594-
assert ref() is None
1589+
assert_gc(lambda: ref() is None)
15951590

15961591

15971592
init_test_cases = [

tests/validators/test_frozenset.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def test_frozenset_no_validators_both(py_and_json: PyAndJson, input_value, expec
8282
('abc', Err('Input should be a valid frozenset')),
8383
],
8484
)
85+
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
8586
def test_frozenset_ints_python(input_value, expected):
8687
v = SchemaValidator({'type': 'frozenset', 'items_schema': {'type': 'int'}})
8788
if isinstance(expected, Err):
@@ -165,6 +166,7 @@ def generate_repeats():
165166
),
166167
],
167168
)
169+
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
168170
def test_frozenset_kwargs_python(kwargs: dict[str, Any], input_value, expected):
169171
v = SchemaValidator({'type': 'frozenset', **kwargs})
170172
if isinstance(expected, Err):

tests/validators/test_list.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def gen_ints():
7171
],
7272
ids=repr,
7373
)
74+
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
7475
def test_list_int(input_value, expected):
7576
v = SchemaValidator({'type': 'list', 'items_schema': {'type': 'int'}})
7677
if isinstance(expected, Err):
@@ -170,6 +171,7 @@ def test_list_error(input_value, index):
170171
),
171172
],
172173
)
174+
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
173175
def test_list_length_constraints(kwargs: dict[str, Any], input_value, expected):
174176
v = SchemaValidator({'type': 'list', **kwargs})
175177
if isinstance(expected, Err):
@@ -511,6 +513,7 @@ class ListInputTestCase:
511513
],
512514
ids=repr,
513515
)
516+
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
514517
def test_list_allowed_inputs_python(testcase: ListInputTestCase):
515518
v = SchemaValidator(core_schema.list_schema(core_schema.int_schema(), strict=testcase.strict))
516519
if isinstance(testcase.output, Err):

0 commit comments

Comments
 (0)