diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index aba1a0d5..b7dbad48 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -35,6 +35,8 @@ jobs: python-version: ${{ matrix.python }} allow-prereleases: true - run: uv run --locked tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }} + - if: endsWith(matrix.python, 't') + run: uv run --locked tox run -e parallel typing: runs-on: ubuntu-latest steps: diff --git a/pyproject.toml b/pyproject.toml index b5e351b8..e7804ffd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ pre-commit = [ ] tests = [ "pytest", + "pytest-run-parallel; python_full_version >= '3.13'", ] typing = [ "mypy", @@ -67,7 +68,10 @@ testpaths = ["tests"] filterwarnings = [ "error", ] - +markers = [ + # Needed when pytest-run-parallel is not installed + "thread_unsafe: mark test as not safe to run in multiple threads", +] [tool.coverage.run] branch = true source = ["markupsafe", "tests"] @@ -124,8 +128,8 @@ tag-only = [ [tool.tox] env_list = [ - "py3.14", "py3.14t", "py3.13", "py3.13t", - "py3.12", "py3.11", "py3.10", "py3.9", + "py3.14", "py3.14t", "parallel", + "py3.13", "py3.13t", "py3.12", "py3.11", "py3.10", "py3.9", "pypy3.11", "style", "typing", @@ -145,6 +149,15 @@ commands = [[ {replace = "posargs", default = [], extend = true}, ]] +[tool.tox.env.parallel] +description = "check for free threading issues" +base_python = ["3.14t"] +commands = [[ + "pytest", "-v", "--tb=short", "--basetemp=env_tmp_dir", + "--parallel-threads=8", + {replace = "posargs", default = [], extend = true}, +]] + [tool.tox.env.style] description = "run all pre-commit hooks on all files" dependency_groups = ["pre-commit"] diff --git a/tests/test_ext_init.py b/tests/test_ext_init.py index f1f6a808..3e535dc3 100644 --- a/tests/test_ext_init.py +++ b/tests/test_ext_init.py @@ -10,6 +10,7 @@ _speedups = None # type: ignore[assignment] +@pytest.mark.thread_unsafe(reason="Tampers with sys.modules") @pytest.mark.skipif(_speedups is None, reason="speedups unavailable") def test_ext_init() -> None: """Test that the extension module uses multi-phase init by checking that diff --git a/tests/test_leak.py b/tests/test_leak.py index b786e072..c9de4751 100644 --- a/tests/test_leak.py +++ b/tests/test_leak.py @@ -2,9 +2,12 @@ import gc +import pytest + from markupsafe import escape +@pytest.mark.thread_unsafe(reason="Tests gc.get_objects()") def test_markup_leaks() -> None: counts = set() # Try to start with a "clean" count. Works for PyPy but not 3.13 JIT. diff --git a/uv.lock b/uv.lock index 4d63ac7f..0110dc46 100644 --- a/uv.lock +++ b/uv.lock @@ -1,8 +1,9 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", "python_full_version == '3.10.*'", "python_full_version < '3.10'", @@ -25,7 +26,8 @@ name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", "python_full_version == '3.10.*'", ] @@ -189,7 +191,8 @@ name = "click" version = "8.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", "python_full_version == '3.10.*'", ] @@ -390,6 +393,7 @@ pre-commit = [ ] tests = [ { name = "pytest" }, + { name = "pytest-run-parallel", marker = "python_full_version >= '3.13'" }, ] typing = [ { name = "mypy" }, @@ -416,7 +420,10 @@ pre-commit = [ { name = "pre-commit" }, { name = "pre-commit-uv" }, ] -tests = [{ name = "pytest" }] +tests = [ + { name = "pytest" }, + { name = "pytest-run-parallel", marker = "python_full_version >= '3.13'" }, +] typing = [ { name = "mypy" }, { name = "pyright" }, @@ -626,6 +633,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] +[[package]] +name = "pytest-run-parallel" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/0c/2dd2d1f2f014e8d9c9f365eb4138c52eeb5b96fa8574f15c1d9436842a48/pytest_run_parallel-0.7.0.tar.gz", hash = "sha256:05088a808d26975f095739a06efc9e8ba4749c194457f9927903eaacdd1e05ce", size = 50185, upload-time = "2025-09-25T13:57:29.686Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/64/d676a217242e263a62bf30588654dba53252fb40df15d8a7023026dae109/pytest_run_parallel-0.7.0-py3-none-any.whl", hash = "sha256:0d8b981a2ac895df25c4bc27b89e8df0bbddbead57cbfdb0aed743db8533a8c0", size = 18884, upload-time = "2025-09-25T13:57:28.545Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -815,7 +834,8 @@ name = "sphinx" version = "8.2.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", ] dependencies = [ @@ -869,7 +889,8 @@ name = "sphinx-autobuild" version = "2025.8.25" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", "python_full_version == '3.11.*'", ] dependencies = [