Skip to content

Commit

Permalink
add test run for 3.13t (#1626)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt authored Feb 6, 2025
1 parent f7fb50b commit 4e824df
Show file tree
Hide file tree
Showing 16 changed files with 89 additions and 52 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,15 @@ jobs:
- '3.11'
- '3.12'
- '3.13'
- '3.13t'
- 'pypy3.9'
- 'pypy3.10'

runs-on: ubuntu-latest

# TODO: get test suite stable with free-threaded python
continue-on-error: ${{ endsWith(matrix.python-version, 't') }}

steps:
- uses: actions/checkout@v4

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

test-os:
name: test on ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ testing = [
'pytest-speed',
'pytest-mock',
'pytest-pretty',
'pytest-run-parallel',
'pytest-timeout',
'python-dateutil',
# numpy doesn't offer prebuilt wheels for all versions and platforms we test in CI e.g. aarch64 musllinux
Expand Down
7 changes: 6 additions & 1 deletion src/argument_markers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ use pyo3::types::{PyDict, PyTuple};

use crate::tools::safe_repr;

#[pyclass(module = "pydantic_core._pydantic_core", get_all, frozen, freelist = 100)]
// see https://github.com/PyO3/pyo3/issues/4894 - freelist is currently unsound with GIL disabled
#[cfg_attr(
not(Py_GIL_DISABLED),
pyclass(module = "pydantic_core._pydantic_core", get_all, frozen, freelist = 100)
)]
#[cfg_attr(Py_GIL_DISABLED, pyclass(module = "pydantic_core._pydantic_core", get_all, frozen))]
#[derive(Debug, Clone)]
pub struct ArgsKwargs {
pub(crate) args: Py<PyTuple>,
Expand Down
21 changes: 20 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from __future__ import annotations as _annotations

import functools
import gc
import importlib.util
import json
import os
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
from time import sleep, time
from typing import Any, Callable, Literal

import hypothesis
import pytest
Expand Down Expand Up @@ -160,3 +162,20 @@ def infinite_generator():
while True:
yield i
i += 1


def assert_gc(test: Callable[[], bool], timeout: float = 10) -> None:
"""Helper to retry garbage collection until the test passes or timeout is
reached.
This is useful on free-threading where the GC collect call finishes before
all cleanup is done.
"""
start = now = time()
while now - start < timeout:
if test():
return
gc.collect()
sleep(0.1)
now = time()
raise AssertionError('Timeout waiting for GC')
2 changes: 2 additions & 0 deletions tests/test_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def find_examples(*_directories):

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

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

@pytest.mark.skipif(CodeExample is None or sys.platform not in {'linux', 'darwin'}, reason='Only on linux and macos')
@pytest.mark.parametrize('example', find_examples('README.md'), ids=str)
@pytest.mark.thread_unsafe # TODO investigate why pytest_examples seems to be thread unsafe here
def test_readme(example: CodeExample, eval_example: EvalExample):
eval_example.set_config(line_length=100, quotes='single')
if eval_example.update_examples:
Expand Down
21 changes: 5 additions & 16 deletions tests/test_garbage_collection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import gc
import platform
from collections.abc import Iterable
from typing import Any
Expand All @@ -8,6 +7,8 @@

from pydantic_core import SchemaSerializer, SchemaValidator, core_schema

from .conftest import assert_gc

GC_TEST_SCHEMA_INNER = core_schema.definitions_schema(
core_schema.definition_reference_schema(schema_ref='model'),
[
Expand Down Expand Up @@ -43,11 +44,7 @@ class MyModel(BaseModel):

del MyModel

gc.collect(0)
gc.collect(1)
gc.collect(2)

assert len(cache) == 0
assert_gc(lambda: len(cache) == 0)


@pytest.mark.xfail(
Expand Down Expand Up @@ -75,11 +72,7 @@ class MyModel(BaseModel):

del MyModel

gc.collect(0)
gc.collect(1)
gc.collect(2)

assert len(cache) == 0
assert_gc(lambda: len(cache) == 0)


@pytest.mark.xfail(
Expand Down Expand Up @@ -114,8 +107,4 @@ def __next__(self):
v.validate_python({'iter': iterable})
del iterable

gc.collect(0)
gc.collect(1)
gc.collect(2)

assert len(cache) == 0
assert_gc(lambda: len(cache) == 0)
9 changes: 9 additions & 0 deletions tests/test_hypothesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ def datetime_schema():


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


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


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

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

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


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


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


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


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


@given(strategies.text())
@pytest.mark.thread_unsafe # https://github.com/Quansight-Labs/pytest-run-parallel/issues/20
def test_serialize_string(str_serializer: SchemaSerializer, data):
assert str_serializer.to_python(data) == data
assert json.loads(str_serializer.to_json(data)) == data
9 changes: 2 additions & 7 deletions tests/validators/test_dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import dataclasses
import gc
import platform
import re
import sys
Expand All @@ -11,7 +10,7 @@

from pydantic_core import ArgsKwargs, SchemaValidator, ValidationError, core_schema

from ..conftest import Err, PyAndJson
from ..conftest import Err, PyAndJson, assert_gc


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

del klass
gc.collect(0)
gc.collect(1)
gc.collect(2)
gc.collect()

assert ref() is None
assert_gc(lambda: ref() is None)


init_test_cases = [
Expand Down
2 changes: 2 additions & 0 deletions tests/validators/test_frozenset.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def test_frozenset_no_validators_both(py_and_json: PyAndJson, input_value, expec
('abc', Err('Input should be a valid frozenset')),
],
)
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
def test_frozenset_ints_python(input_value, expected):
v = SchemaValidator({'type': 'frozenset', 'items_schema': {'type': 'int'}})
if isinstance(expected, Err):
Expand Down Expand Up @@ -165,6 +166,7 @@ def generate_repeats():
),
],
)
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
def test_frozenset_kwargs_python(kwargs: dict[str, Any], input_value, expected):
v = SchemaValidator({'type': 'frozenset', **kwargs})
if isinstance(expected, Err):
Expand Down
3 changes: 3 additions & 0 deletions tests/validators/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def gen_ints():
],
ids=repr,
)
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
def test_list_int(input_value, expected):
v = SchemaValidator({'type': 'list', 'items_schema': {'type': 'int'}})
if isinstance(expected, Err):
Expand Down Expand Up @@ -170,6 +171,7 @@ def test_list_error(input_value, index):
),
],
)
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
def test_list_length_constraints(kwargs: dict[str, Any], input_value, expected):
v = SchemaValidator({'type': 'list', **kwargs})
if isinstance(expected, Err):
Expand Down Expand Up @@ -511,6 +513,7 @@ class ListInputTestCase:
],
ids=repr,
)
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
def test_list_allowed_inputs_python(testcase: ListInputTestCase):
v = SchemaValidator(core_schema.list_schema(core_schema.int_schema(), strict=testcase.strict))
if isinstance(testcase.output, Err):
Expand Down
9 changes: 3 additions & 6 deletions tests/validators/test_model_init.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import gc
import platform
import weakref

Expand All @@ -7,6 +6,8 @@

from pydantic_core import CoreConfig, SchemaValidator, core_schema

from ..conftest import assert_gc


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

del klass
gc.collect(0)
gc.collect(1)
gc.collect(2)
gc.collect()

assert ref() is None
assert_gc(lambda: ref() is None)


def test_model_custom_init_with_union() -> None:
Expand Down
9 changes: 3 additions & 6 deletions tests/validators/test_nullable.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import gc
import platform
import weakref

import pytest

from pydantic_core import SchemaValidator, ValidationError, core_schema

from ..conftest import assert_gc


def test_nullable():
v = SchemaValidator({'type': 'nullable', 'schema': {'type': 'int'}})
Expand Down Expand Up @@ -62,9 +63,5 @@ def validate(v, info):
assert ref() is not None

del cycle
gc.collect(0)
gc.collect(1)
gc.collect(2)
gc.collect()

assert ref() is None
assert_gc(lambda: ref() is None)
2 changes: 2 additions & 0 deletions tests/validators/test_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def test_frozenset_no_validators_both(py_and_json: PyAndJson, input_value, expec
('abc', Err('Input should be a valid set')),
],
)
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
def test_set_ints_python(input_value, expected):
v = SchemaValidator({'type': 'set', 'items_schema': {'type': 'int'}})
if isinstance(expected, Err):
Expand Down Expand Up @@ -162,6 +163,7 @@ def generate_repeats():
],
ids=repr,
)
@pytest.mark.thread_unsafe # generators in parameters not compatible with pytest-run-parallel, https://github.com/Quansight-Labs/pytest-run-parallel/issues/14
def test_set_kwargs(kwargs: dict[str, Any], input_value, expected):
v = SchemaValidator({'type': 'set', **kwargs})
if isinstance(expected, Err):
Expand Down
9 changes: 2 additions & 7 deletions tests/validators/test_typed_dict.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import gc
import math
import platform
import re
Expand All @@ -11,7 +10,7 @@

from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, core_schema, validate_core_schema

from ..conftest import Err, PyAndJson
from ..conftest import Err, PyAndJson, assert_gc


class Cls:
Expand Down Expand Up @@ -1191,9 +1190,5 @@ def validate(v, info):
assert ref() is not None

del cycle
gc.collect(0)
gc.collect(1)
gc.collect(2)
gc.collect()

assert ref() is None
assert_gc(lambda: ref() is None)
Loading

0 comments on commit 4e824df

Please sign in to comment.