From 3e28022da2cc4fab0a3bd9e9f8193123b7558306 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 6 Feb 2025 11:17:33 +0000 Subject: [PATCH 1/5] add test run for 3.13t --- .github/workflows/ci.yml | 5 +++++ pyproject.toml | 3 +++ src/argument_markers.rs | 6 +++++- src/lib.rs | 2 +- tests/conftest.py | 21 ++++++++++++++++++++- tests/test_garbage_collection.py | 21 +++++---------------- tests/validators/test_dataclasses.py | 9 ++------- tests/validators/test_frozenset.py | 2 ++ tests/validators/test_list.py | 3 +++ tests/validators/test_model_init.py | 9 +++------ tests/validators/test_nullable.py | 9 +++------ tests/validators/test_set.py | 1 + tests/validators/test_typed_dict.py | 9 ++------- tests/validators/test_with_default.py | 10 ++-------- uv.lock | 2 ++ 15 files changed, 59 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c23f784eb..745c8cd05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,7 @@ jobs: - '3.11' - '3.12' - '3.13' + - '3.13t' - 'pypy3.9' - 'pypy3.10' @@ -96,11 +97,15 @@ jobs: env: RUST_BACKTRACE: 1 + - if: endsWith(matrix.python-version, 't') + run: uv pip install pytest-run-parallel + - run: uv pip freeze - run: uv run pytest env: HYPOTHESIS_PROFILE: slow + PYTEST_ADDOPTS: ${{ endsWith(matrix.python-version, 't') && '--parallel-threads=2' || '' }} test-os: name: test on ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index 27b54938d..429aad1d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,9 @@ filterwarnings = [ # Python 3.9 and below allowed truncation of float to integers in some # cases, by not making this an error we can test for this behaviour 'ignore:(.+)Implicit conversion to integers using __int__ is deprecated', + # free-threading seems to upset Hypothesis + 'ignore:The recursion limit will not be reset, since it was changed from another thread or during execution of a test.', + 'ignore:Do not use the `random` module inside strategies; instead consider `st.randoms()`, `st.sampled_from()`, etc. from data=datetimes()', ] timeout = 30 xfail_strict = true diff --git a/src/argument_markers.rs b/src/argument_markers.rs index f22a32c78..8cff08620 100644 --- a/src/argument_markers.rs +++ b/src/argument_markers.rs @@ -5,7 +5,11 @@ use pyo3::types::{PyDict, PyTuple}; use crate::tools::safe_repr; -#[pyclass(module = "pydantic_core._pydantic_core", get_all, frozen, freelist = 100)] +#[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, diff --git a/src/lib.rs b/src/lib.rs index 7689f3988..9ec2cac30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,7 +106,7 @@ pub fn build_info() -> String { ) } -#[pymodule] +#[pymodule(gil_used = false)] fn _pydantic_core(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("__version__", get_pydantic_core_version())?; m.add("build_profile", env!("PROFILE"))?; diff --git a/tests/conftest.py b/tests/conftest.py index f41d36b88..56d94a0c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 @@ -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') diff --git a/tests/test_garbage_collection.py b/tests/test_garbage_collection.py index 2b26bf008..512e480c5 100644 --- a/tests/test_garbage_collection.py +++ b/tests/test_garbage_collection.py @@ -1,4 +1,3 @@ -import gc import platform from collections.abc import Iterable from typing import Any @@ -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'), [ @@ -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( @@ -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( @@ -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) diff --git a/tests/validators/test_dataclasses.py b/tests/validators/test_dataclasses.py index 2555c733d..09bd4e762 100644 --- a/tests/validators/test_dataclasses.py +++ b/tests/validators/test_dataclasses.py @@ -1,5 +1,4 @@ import dataclasses -import gc import platform import re import sys @@ -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( @@ -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 = [ diff --git a/tests/validators/test_frozenset.py b/tests/validators/test_frozenset.py index b17feb95a..c75175766 100644 --- a/tests/validators/test_frozenset.py +++ b/tests/validators/test_frozenset.py @@ -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): @@ -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): diff --git a/tests/validators/test_list.py b/tests/validators/test_list.py index ac5c5c051..991a90cbc 100644 --- a/tests/validators/test_list.py +++ b/tests/validators/test_list.py @@ -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): @@ -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): @@ -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): diff --git a/tests/validators/test_model_init.py b/tests/validators/test_model_init.py index b0c28dc86..f1226265a 100644 --- a/tests/validators/test_model_init.py +++ b/tests/validators/test_model_init.py @@ -1,4 +1,3 @@ -import gc import platform import weakref @@ -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__` @@ -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: diff --git a/tests/validators/test_nullable.py b/tests/validators/test_nullable.py index a74d56138..451559c76 100644 --- a/tests/validators/test_nullable.py +++ b/tests/validators/test_nullable.py @@ -1,4 +1,3 @@ -import gc import platform import weakref @@ -6,6 +5,8 @@ from pydantic_core import SchemaValidator, ValidationError, core_schema +from ..conftest import assert_gc + def test_nullable(): v = SchemaValidator({'type': 'nullable', 'schema': {'type': 'int'}}) @@ -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) diff --git a/tests/validators/test_set.py b/tests/validators/test_set.py index b5d35abf3..d36d20888 100644 --- a/tests/validators/test_set.py +++ b/tests/validators/test_set.py @@ -146,6 +146,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): diff --git a/tests/validators/test_typed_dict.py b/tests/validators/test_typed_dict.py index 9d543ea4f..b41a1188f 100644 --- a/tests/validators/test_typed_dict.py +++ b/tests/validators/test_typed_dict.py @@ -1,4 +1,3 @@ -import gc import math import platform import re @@ -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: @@ -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) diff --git a/tests/validators/test_with_default.py b/tests/validators/test_with_default.py index a27b80268..7c5972c5a 100644 --- a/tests/validators/test_with_default.py +++ b/tests/validators/test_with_default.py @@ -1,4 +1,3 @@ -import gc import platform import sys import weakref @@ -17,7 +16,7 @@ core_schema, ) -from ..conftest import PyAndJson +from ..conftest import PyAndJson, assert_gc def test_typed_dict_default(): @@ -662,12 +661,7 @@ def _validator(cls, v, 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) validate_default_raises_examples = [ diff --git a/uv.lock b/uv.lock index 3e936fef1..3e1d672ec 100644 --- a/uv.lock +++ b/uv.lock @@ -601,6 +601,7 @@ dev = [ ] linting = [ { name = "griffe" }, + { name = "maturin" }, { name = "mypy" }, { name = "pyright" }, { name = "ruff" }, @@ -663,6 +664,7 @@ codspeed = [{ name = "pytest-codspeed", marker = "python_full_version == '3.13.* dev = [{ name = "maturin" }] linting = [ { name = "griffe" }, + { name = "maturin" }, { name = "mypy" }, { name = "pyright" }, { name = "ruff" }, From 2e7717e472258fec33902e577e2dd28289c5f317 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 6 Feb 2025 11:40:58 +0000 Subject: [PATCH 2/5] adjustments, allow failure for now --- .github/workflows/ci.yml | 8 +++++--- pyproject.toml | 5 +++-- src/argument_markers.rs | 1 + src/lib.rs | 2 +- tests/validators/test_set.py | 1 + uv.lock | 16 ++++++++++++++++ 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 745c8cd05..3f4beaae9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,9 @@ jobs: 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 @@ -97,15 +100,14 @@ jobs: env: RUST_BACKTRACE: 1 - - if: endsWith(matrix.python-version, 't') - run: uv pip install pytest-run-parallel - - run: uv pip freeze - run: uv run pytest env: HYPOTHESIS_PROFILE: slow PYTEST_ADDOPTS: ${{ endsWith(matrix.python-version, 't') && '--parallel-threads=2' || '' }} + # 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 }} diff --git a/pyproject.toml b/pyproject.toml index 429aad1d9..388930cc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 @@ -108,8 +109,8 @@ filterwarnings = [ # cases, by not making this an error we can test for this behaviour 'ignore:(.+)Implicit conversion to integers using __int__ is deprecated', # free-threading seems to upset Hypothesis - 'ignore:The recursion limit will not be reset, since it was changed from another thread or during execution of a test.', - 'ignore:Do not use the `random` module inside strategies; instead consider `st.randoms()`, `st.sampled_from()`, etc. from data=datetimes()', + 'ignore:(.*)The recursion limit will not be reset, since it was changed from another thread or during execution of a test.(.*)', + 'ignore:(.*)Do not use the `random` module inside strategies(.*)', ] timeout = 30 xfail_strict = true diff --git a/src/argument_markers.rs b/src/argument_markers.rs index 8cff08620..1b74a62f1 100644 --- a/src/argument_markers.rs +++ b/src/argument_markers.rs @@ -5,6 +5,7 @@ use pyo3::types::{PyDict, PyTuple}; use crate::tools::safe_repr; +// 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) diff --git a/src/lib.rs b/src/lib.rs index 9ec2cac30..7689f3988 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,7 +106,7 @@ pub fn build_info() -> String { ) } -#[pymodule(gil_used = false)] +#[pymodule] fn _pydantic_core(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("__version__", get_pydantic_core_version())?; m.add("build_profile", env!("PROFILE"))?; diff --git a/tests/validators/test_set.py b/tests/validators/test_set.py index d36d20888..d753ba02a 100644 --- a/tests/validators/test_set.py +++ b/tests/validators/test_set.py @@ -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): diff --git a/uv.lock b/uv.lock index 3e1d672ec..7ad8e1c5a 100644 --- a/uv.lock +++ b/uv.lock @@ -586,6 +586,7 @@ all = [ { name = "pytest-examples", marker = "implementation_name == 'cpython' and platform_machine == 'x86_64'" }, { name = "pytest-mock" }, { name = "pytest-pretty" }, + { name = "pytest-run-parallel" }, { name = "pytest-speed" }, { name = "pytest-timeout" }, { name = "python-dateutil" }, @@ -620,6 +621,7 @@ testing = [ { name = "pytest-examples", marker = "implementation_name == 'cpython' and platform_machine == 'x86_64'" }, { name = "pytest-mock" }, { name = "pytest-pretty" }, + { name = "pytest-run-parallel" }, { name = "pytest-speed" }, { name = "pytest-timeout" }, { name = "python-dateutil" }, @@ -653,6 +655,7 @@ all = [ { name = "pytest-examples", marker = "implementation_name == 'cpython' and platform_machine == 'x86_64'" }, { name = "pytest-mock" }, { name = "pytest-pretty" }, + { name = "pytest-run-parallel" }, { name = "pytest-speed" }, { name = "pytest-timeout" }, { name = "python-dateutil" }, @@ -683,6 +686,7 @@ testing = [ { name = "pytest-examples", marker = "implementation_name == 'cpython' and platform_machine == 'x86_64'" }, { name = "pytest-mock" }, { name = "pytest-pretty" }, + { name = "pytest-run-parallel" }, { name = "pytest-speed" }, { name = "pytest-timeout" }, { name = "python-dateutil" }, @@ -797,6 +801,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/fe/d44d391312c1b8abee2af58ee70fabb1c00b6577ac4e0bdf25b70c1caffb/pytest_pretty-1.2.0-py3-none-any.whl", hash = "sha256:6f79122bf53864ae2951b6c9e94d7a06a87ef753476acd4588aeac018f062036", size = 6180 }, ] +[[package]] +name = "pytest-run-parallel" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/13/d3bcb2c52f63b774be612cbee51f33a5cf2fa97d2f60fe26264f6720477b/pytest_run_parallel-0.3.1.tar.gz", hash = "sha256:636306d3ed6838898d8d42b3cd379dac7b327ce6d68df1bcc30d55a208d5081e", size = 14142 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/67/4943178bb5dacb2e0b745b4db4ab126112dafab66266f2262896e791dbbe/pytest_run_parallel-0.3.1-py3-none-any.whl", hash = "sha256:0675c9e4c8e843085333c66bc0ce6b8091e3509dc8e6df3429f05c28f5345b17", size = 9468 }, +] + [[package]] name = "pytest-speed" version = "0.3.5" From 7bd36e7aba5d42202b0383fd5d33cc2afb2531cc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 6 Feb 2025 11:45:33 +0000 Subject: [PATCH 3/5] fail 3.13t tests fast for easier debugging --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f4beaae9..3d7dc1201 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,7 +105,9 @@ jobs: - run: uv run pytest env: HYPOTHESIS_PROFILE: slow - PYTEST_ADDOPTS: ${{ endsWith(matrix.python-version, 't') && '--parallel-threads=2' || '' }} + # 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' }} From b8e1006437f84d1130ba4333a4494d5a84050108 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 6 Feb 2025 11:50:46 +0000 Subject: [PATCH 4/5] mark `pytest_examples` as thread unsafe --- tests/test_docstrings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index 25f5bf1e4..b3703067b 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -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') @@ -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: From d1b277d1c1c5bc1c7b8cc32d2da5c8a72e73f027 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 6 Feb 2025 11:57:22 +0000 Subject: [PATCH 5/5] mark `hypothesis` as thread unsafe --- pyproject.toml | 3 --- tests/test_hypothesis.py | 9 +++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 388930cc2..db64e0d7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,9 +108,6 @@ filterwarnings = [ # Python 3.9 and below allowed truncation of float to integers in some # cases, by not making this an error we can test for this behaviour 'ignore:(.+)Implicit conversion to integers using __int__ is deprecated', - # free-threading seems to upset Hypothesis - 'ignore:(.*)The recursion limit will not be reset, since it was changed from another thread or during execution of a test.(.*)', - 'ignore:(.*)Do not use the `random` module inside strategies(.*)', ] timeout = 30 xfail_strict = true diff --git a/tests/test_hypothesis.py b/tests/test_hypothesis.py index f02d1b3a9..51674f620 100644 --- a/tests/test_hypothesis.py +++ b/tests/test_hypothesis.py @@ -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: @@ -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) @@ -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 @@ -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 @@ -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 @@ -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) @@ -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) @@ -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