Skip to content

Commit 3e28022

Browse files
committed
add test run for 3.13t
1 parent 164b9ff commit 3e28022

File tree

15 files changed

+59
-53
lines changed

15 files changed

+59
-53
lines changed

.github/workflows/ci.yml

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

@@ -96,11 +97,15 @@ jobs:
9697
env:
9798
RUST_BACKTRACE: 1
9899

100+
- if: endsWith(matrix.python-version, 't')
101+
run: uv pip install pytest-run-parallel
102+
99103
- run: uv pip freeze
100104

101105
- run: uv run pytest
102106
env:
103107
HYPOTHESIS_PROFILE: slow
108+
PYTEST_ADDOPTS: ${{ endsWith(matrix.python-version, 't') && '--parallel-threads=2' || '' }}
104109

105110
test-os:
106111
name: test on ${{ matrix.os }}

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ filterwarnings = [
107107
# Python 3.9 and below allowed truncation of float to integers in some
108108
# cases, by not making this an error we can test for this behaviour
109109
'ignore:(.+)Implicit conversion to integers using __int__ is deprecated',
110+
# free-threading seems to upset Hypothesis
111+
'ignore:The recursion limit will not be reset, since it was changed from another thread or during execution of a test.',
112+
'ignore:Do not use the `random` module inside strategies; instead consider `st.randoms()`, `st.sampled_from()`, etc. from data=datetimes()',
110113
]
111114
timeout = 30
112115
xfail_strict = true

src/argument_markers.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ 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+
#[cfg_attr(
9+
not(Py_GIL_DISABLED),
10+
pyclass(module = "pydantic_core._pydantic_core", get_all, frozen, freelist = 100)
11+
)]
12+
#[cfg_attr(Py_GIL_DISABLED, pyclass(module = "pydantic_core._pydantic_core", get_all, frozen))]
913
#[derive(Debug, Clone)]
1014
pub struct ArgsKwargs {
1115
pub(crate) args: Py<PyTuple>,

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pub fn build_info() -> String {
106106
)
107107
}
108108

109-
#[pymodule]
109+
#[pymodule(gil_used = false)]
110110
fn _pydantic_core(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
111111
m.add("__version__", get_pydantic_core_version())?;
112112
m.add("build_profile", env!("PROFILE"))?;

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_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/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):

tests/validators/test_model_init.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import gc
21
import platform
32
import weakref
43

@@ -7,6 +6,8 @@
76

87
from pydantic_core import CoreConfig, SchemaValidator, core_schema
98

9+
from ..conftest import assert_gc
10+
1011

1112
class MyModel:
1213
# this is not required, but it avoids `__pydantic_fields_set__` being included in `__dict__`
@@ -473,12 +474,8 @@ def _wrap_validator(cls, v, validator, info):
473474
assert ref() is not None
474475

475476
del klass
476-
gc.collect(0)
477-
gc.collect(1)
478-
gc.collect(2)
479-
gc.collect()
480477

481-
assert ref() is None
478+
assert_gc(lambda: ref() is None)
482479

483480

484481
def test_model_custom_init_with_union() -> None:

0 commit comments

Comments
 (0)