pytest-threadpool 0.2.0__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/.github/workflows/ci.yml +5 -0
- pytest_threadpool-0.3.3/.github/workflows/publish.yml +25 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/.gitignore +3 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/CONTRIBUTING.md +36 -21
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/PKG-INFO +25 -6
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/README.md +23 -4
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/pyproject.toml +12 -4
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_fixtures.py +68 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_runner.py +219 -62
- pytest_threadpool-0.3.3/src/pytest_threadpool/_version.py +34 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/collected_count.py +23 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_addfinalizer.py +49 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_chain.py +54 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_multiple_per_test.py +65 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_setup_parallel.py +51 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_with_shared_deps.py +61 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_with_tmp_path.py +45 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_with_xunit.py +55 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_yield_teardown.py +47 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_after_test_failure.py +38 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_parallel_timing.py +55 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_same_thread.py +50 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_xunit_function.py +51 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_xunit_method_thread.py +49 -0
- pytest_threadpool-0.3.3/tests/integration_tests/cases/xunit_method_teardown_runs.py +43 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/conftest.py +9 -0
- pytest_threadpool-0.3.3/tests/integration_tests/test_pytester_func_fixtures.py +69 -0
- pytest_threadpool-0.3.3/tests/integration_tests/test_pytester_parallel_teardown.py +42 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_reporting.py +9 -0
- pytest_threadpool-0.2.0/.claude/settings.local.json +0 -30
- pytest_threadpool-0.2.0/ANALYSIS.md +0 -142
- pytest_threadpool-0.2.0/CLAUDE.md +0 -0
- pytest_threadpool-0.2.0/PLAN.md +0 -222
- pytest_threadpool-0.2.0/TODO.md +0 -44
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/LICENSE +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/hooks/pre-commit +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/scripts/setup-dev +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_api.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_constants.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_grouping.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_markers.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/plugin.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/_templates.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/class_single_method.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/class_thread_verification.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_sigint.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_system_exit.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/marks_custom.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/marks_standard.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_incremental.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_package_children.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_all_merged.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_module_children.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_parameters.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/setup_all_fail.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_counter.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/validate_threadpool.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_class.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_edge_cases.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_fixtures.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_marks.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_package.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_scopes.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_sequential.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_shared.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_xunit.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/__init__.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_api.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_fixtures.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_grouping.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_markers.py +0 -0
- {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_plugin.py +0 -0
|
@@ -5,12 +5,15 @@ on:
|
|
|
5
5
|
branches: [main]
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [main]
|
|
8
|
+
workflow_call:
|
|
8
9
|
|
|
9
10
|
jobs:
|
|
10
11
|
lint:
|
|
11
12
|
runs-on: ubuntu-latest
|
|
12
13
|
steps:
|
|
13
14
|
- uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0
|
|
14
17
|
- uses: astral-sh/setup-uv@v6
|
|
15
18
|
- run: uv sync --python 3.14t --group dev
|
|
16
19
|
- run: uv run ruff format --check src/ tests/
|
|
@@ -25,6 +28,8 @@ jobs:
|
|
|
25
28
|
python: ["3.13t", "3.14t", "3.15t"]
|
|
26
29
|
steps:
|
|
27
30
|
- uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
fetch-depth: 0
|
|
28
33
|
- uses: astral-sh/setup-uv@v6
|
|
29
34
|
- run: uv python install ${{ matrix.python }}
|
|
30
35
|
- run: uv sync --python ${{ matrix.python }} --group dev
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
ci:
|
|
9
|
+
uses: ./.github/workflows/ci.yml
|
|
10
|
+
secrets: inherit
|
|
11
|
+
|
|
12
|
+
publish:
|
|
13
|
+
needs: ci
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
- uses: astral-sh/setup-uv@v6
|
|
23
|
+
- run: uv build
|
|
24
|
+
- name: Publish to PyPI
|
|
25
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -40,10 +40,13 @@ Any mutable state needed across tests or across a test run must be held as
|
|
|
40
40
|
class attributes or instance attributes — never as module-level variables.
|
|
41
41
|
|
|
42
42
|
```python
|
|
43
|
+
import threading
|
|
44
|
+
|
|
43
45
|
# Good
|
|
44
46
|
class TestState:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
log = {}
|
|
48
|
+
barrier = threading.Barrier(3, timeout=10)
|
|
49
|
+
|
|
47
50
|
|
|
48
51
|
# Bad
|
|
49
52
|
_log = {}
|
|
@@ -67,9 +70,11 @@ subprocess with the exact thread count needed. Use explicit thread counts for
|
|
|
67
70
|
barrier-based tests (not `auto`) to avoid deadlocks on low-core machines.
|
|
68
71
|
|
|
69
72
|
```python
|
|
73
|
+
import threading
|
|
74
|
+
|
|
70
75
|
# Good — isolated subprocess via ftdir, explicit thread count
|
|
71
76
|
def test_children_run_concurrently(self, ftdir):
|
|
72
|
-
|
|
77
|
+
ftdir.makepyfile("""
|
|
73
78
|
import threading, pytest
|
|
74
79
|
@pytest.mark.parallelizable("children")
|
|
75
80
|
class TestBarrier:
|
|
@@ -77,16 +82,25 @@ def test_children_run_concurrently(self, ftdir):
|
|
|
77
82
|
def test_a(self): self.barrier.wait()
|
|
78
83
|
def test_b(self): self.barrier.wait()
|
|
79
84
|
""")
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
result = ftdir.run_pytest("--threadpool", "2")
|
|
86
|
+
result.assert_outcomes(passed=2)
|
|
87
|
+
|
|
82
88
|
|
|
83
89
|
# Bad — depends on outer runner flags
|
|
84
90
|
class TestBarrier:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
barrier = threading.Barrier(2, timeout=10)
|
|
92
|
+
|
|
93
|
+
def test_a(self): self.barrier.wait()
|
|
94
|
+
|
|
95
|
+
def test_b(self): self.barrier.wait()
|
|
88
96
|
```
|
|
89
97
|
|
|
98
|
+
### Use `pytest.fail()` for intentional failures
|
|
99
|
+
|
|
100
|
+
In test cases that need to intentionally fail (e.g., verifying teardown after
|
|
101
|
+
failure), use `pytest.fail("reason")` instead of `assert False, "reason"`.
|
|
102
|
+
The latter triggers ruff PT015 and B011.
|
|
103
|
+
|
|
90
104
|
### Constants over strings
|
|
91
105
|
|
|
92
106
|
Use `ParallelScope` enum and `_constants` module instead of bare string
|
|
@@ -107,7 +121,7 @@ src/pytest_threadpool/
|
|
|
107
121
|
_constants.py # ParallelScope and _GroupPrefix enums
|
|
108
122
|
_markers.py # MarkerResolver: marker introspection
|
|
109
123
|
_grouping.py # GroupKeyBuilder: parallel batch grouping
|
|
110
|
-
_fixtures.py # FixtureManager: finalizer save/restore
|
|
124
|
+
_fixtures.py # FixtureManager: fixture cloning, shared cache population, finalizer save/restore
|
|
111
125
|
_runner.py # ParallelRunner: parallel execution orchestration
|
|
112
126
|
plugin.py # pytest hook implementations (wiring only)
|
|
113
127
|
|
|
@@ -128,18 +142,19 @@ Every commit message starts with one or more tags in brackets:
|
|
|
128
142
|
|
|
129
143
|
### Tags
|
|
130
144
|
|
|
131
|
-
| Tag
|
|
132
|
-
|
|
133
|
-
| `[Runner]`
|
|
134
|
-
| `[Markers]`
|
|
135
|
-
| `[Report]`
|
|
136
|
-
| `[Plugin]`
|
|
137
|
-
| `[API]`
|
|
138
|
-
| `[Test]`
|
|
139
|
-
| `[
|
|
140
|
-
| `[
|
|
141
|
-
| `[
|
|
142
|
-
| `[
|
|
145
|
+
| Tag | Scope |
|
|
146
|
+
|--------------|---------------------------------------------------------------------------------|
|
|
147
|
+
| `[Runner]` | Core parallel execution engine (`_runner.py`, `_fixtures.py`) |
|
|
148
|
+
| `[Markers]` | Marker resolution and grouping (`_markers.py`, `_grouping.py`, `_constants.py`) |
|
|
149
|
+
| `[Report]` | Terminal reporting and display (`_LiveReporter`) |
|
|
150
|
+
| `[Plugin]` | Plugin hooks and CLI options (`plugin.py`) |
|
|
151
|
+
| `[API]` | Public API changes (`_api.py`, `__init__.py`) |
|
|
152
|
+
| `[Test]` | Test additions or changes only |
|
|
153
|
+
| `[Build]` | CI/CD pipelines, publishing, versioning |
|
|
154
|
+
| `[Tooling]` | Ruff, pyright, pre-commit, scripts |
|
|
155
|
+
| `[Docs]` | README, CONTRIBUTING, docstrings |
|
|
156
|
+
| `[Refactor]` | Internal restructuring, no behavior change |
|
|
157
|
+
| `[Fix]` | Bug fix — must include issue number if fixing a known issue |
|
|
143
158
|
|
|
144
159
|
### Combining tags
|
|
145
160
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-threadpool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Parallel test execution for free-threaded Python builds
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
|
-
Keywords: free-threaded,nogil,parallel,pytest,testing,threadpool
|
|
7
|
+
Keywords: free-threaded,nogil,parallel,pytest,pytest parallel,testing,threadpool
|
|
8
8
|
Classifier: Development Status :: 4 - Beta
|
|
9
9
|
Classifier: Framework :: Pytest
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
@@ -17,8 +17,8 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
|
|
18
18
|
# pytest-threadpool
|
|
19
19
|
|
|
20
|
-
[](https://pypi.org/project/pytest-threadpool/)
|
|
21
|
+
[](https://pypi.org/project/pytest-threadpool/)
|
|
22
22
|
[](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
|
|
23
23
|
[](https://github.com/pytest-threadpool/pytest-threadpool)
|
|
24
24
|
[](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml)
|
|
@@ -26,8 +26,9 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
**Status: Beta** · Parallel test execution for free-threaded Python builds (3.13t+).
|
|
28
28
|
|
|
29
|
-
Runs test *bodies
|
|
30
|
-
|
|
29
|
+
Runs test *bodies*, function-scoped fixture setup, and function-scoped fixture
|
|
30
|
+
teardown concurrently in a thread pool while keeping shared fixtures
|
|
31
|
+
(module/class/session scope) sequential.
|
|
31
32
|
|
|
32
33
|
## Installation
|
|
33
34
|
|
|
@@ -75,6 +76,24 @@ def test_must_be_sequential(): ...
|
|
|
75
76
|
| `parameters` | Parametrized variants of the same test run concurrently |
|
|
76
77
|
| `all` | Combines `children` + `parameters` |
|
|
77
78
|
|
|
79
|
+
## Fixture handling
|
|
80
|
+
|
|
81
|
+
Function-scoped fixtures are cloned per-item and set up in parallel alongside
|
|
82
|
+
test calls. Each worker gets independent fixture instances — no shared mutable
|
|
83
|
+
state between concurrent fixture setups.
|
|
84
|
+
|
|
85
|
+
Shared fixtures (module, class, and session scope) are resolved once
|
|
86
|
+
sequentially before workers launch and served from cache to all items.
|
|
87
|
+
|
|
88
|
+
| Scope | Behavior |
|
|
89
|
+
|------------|-------------------------------------------------|
|
|
90
|
+
| `function` | Cloned per-item, setup and teardown in parallel workers |
|
|
91
|
+
| `class` | Resolved once, cached, shared across items |
|
|
92
|
+
| `module` | Resolved once, cached, shared across items |
|
|
93
|
+
| `session` | Resolved once, cached, shared across items |
|
|
94
|
+
|
|
95
|
+
Shared fixture teardown runs sequentially after the parallel group completes.
|
|
96
|
+
|
|
78
97
|
## Marker levels
|
|
79
98
|
|
|
80
99
|
Markers can be applied at function, class, module (`pytestmark`), or
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pytest-threadpool
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/pytest-threadpool/)
|
|
4
|
+
[](https://pypi.org/project/pytest-threadpool/)
|
|
5
5
|
[](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
|
|
6
6
|
[](https://github.com/pytest-threadpool/pytest-threadpool)
|
|
7
7
|
[](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml)
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
**Status: Beta** · Parallel test execution for free-threaded Python builds (3.13t+).
|
|
11
11
|
|
|
12
|
-
Runs test *bodies
|
|
13
|
-
|
|
12
|
+
Runs test *bodies*, function-scoped fixture setup, and function-scoped fixture
|
|
13
|
+
teardown concurrently in a thread pool while keeping shared fixtures
|
|
14
|
+
(module/class/session scope) sequential.
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -58,6 +59,24 @@ def test_must_be_sequential(): ...
|
|
|
58
59
|
| `parameters` | Parametrized variants of the same test run concurrently |
|
|
59
60
|
| `all` | Combines `children` + `parameters` |
|
|
60
61
|
|
|
62
|
+
## Fixture handling
|
|
63
|
+
|
|
64
|
+
Function-scoped fixtures are cloned per-item and set up in parallel alongside
|
|
65
|
+
test calls. Each worker gets independent fixture instances — no shared mutable
|
|
66
|
+
state between concurrent fixture setups.
|
|
67
|
+
|
|
68
|
+
Shared fixtures (module, class, and session scope) are resolved once
|
|
69
|
+
sequentially before workers launch and served from cache to all items.
|
|
70
|
+
|
|
71
|
+
| Scope | Behavior |
|
|
72
|
+
|------------|-------------------------------------------------|
|
|
73
|
+
| `function` | Cloned per-item, setup and teardown in parallel workers |
|
|
74
|
+
| `class` | Resolved once, cached, shared across items |
|
|
75
|
+
| `module` | Resolved once, cached, shared across items |
|
|
76
|
+
| `session` | Resolved once, cached, shared across items |
|
|
77
|
+
|
|
78
|
+
Shared fixture teardown runs sequentially after the parallel group completes.
|
|
79
|
+
|
|
61
80
|
## Marker levels
|
|
62
81
|
|
|
63
82
|
Markers can be applied at function, class, module (`pytestmark`), or
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytest-threadpool"
|
|
3
|
-
|
|
3
|
+
dynamic = ["version"]
|
|
4
4
|
description = "Parallel test execution for free-threaded Python builds"
|
|
5
5
|
requires-python = ">=3.13"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
readme = "README.md"
|
|
8
|
-
keywords = ["pytest", "parallel", "free-threaded", "nogil", "testing", "threadpool"]
|
|
8
|
+
keywords = ["pytest", "parallel", "free-threaded", "nogil", "testing", "threadpool", "pytest parallel"]
|
|
9
9
|
classifiers = [
|
|
10
10
|
"Framework :: Pytest",
|
|
11
11
|
"Development Status :: 4 - Beta",
|
|
@@ -29,24 +29,32 @@ dev = [
|
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[build-system]
|
|
32
|
-
requires = ["hatchling"]
|
|
32
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
33
33
|
build-backend = "hatchling.build"
|
|
34
34
|
|
|
35
|
+
[tool.hatch.version]
|
|
36
|
+
source = "vcs"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.hooks.vcs]
|
|
39
|
+
version-file = "src/pytest_threadpool/_version.py"
|
|
40
|
+
|
|
35
41
|
[tool.hatch.build.targets.wheel]
|
|
36
42
|
packages = ["src/pytest_threadpool"]
|
|
37
43
|
|
|
38
44
|
[tool.pytest.ini_options]
|
|
39
45
|
addopts = ["--threadpool", "auto"]
|
|
46
|
+
norecursedirs = ["cases"]
|
|
40
47
|
|
|
41
48
|
[tool.pyright]
|
|
42
49
|
pythonVersion = "3.13"
|
|
43
50
|
include = ["src", "tests"]
|
|
44
51
|
typeCheckingMode = "standard"
|
|
45
52
|
reportPrivateUsage = false
|
|
46
|
-
ignore = ["tests/unit_tests"]
|
|
53
|
+
ignore = ["tests/unit_tests", "src/pytest_threadpool/_version.py"]
|
|
47
54
|
|
|
48
55
|
[tool.ruff]
|
|
49
56
|
target-version = "py313"
|
|
57
|
+
exclude = ["src/pytest_threadpool/_version.py"]
|
|
50
58
|
src = ["src", "tests"]
|
|
51
59
|
line-length = 99
|
|
52
60
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Fixture finalizer save/restore helpers for parallel execution."""
|
|
2
2
|
|
|
3
|
+
from _pytest.fixtures import FixtureDef
|
|
3
4
|
from _pytest.scope import Scope
|
|
4
5
|
|
|
5
6
|
|
|
@@ -12,6 +13,40 @@ class FixtureManager:
|
|
|
12
13
|
runner.py and fixtures.py use.
|
|
13
14
|
"""
|
|
14
15
|
|
|
16
|
+
@staticmethod
|
|
17
|
+
def clone_function_fixturedefs(item) -> None:
|
|
18
|
+
"""Clone function-scoped FixtureDefs so this item has its own copies.
|
|
19
|
+
|
|
20
|
+
Shared (module/class/session) FixtureDefs are kept as-is — their
|
|
21
|
+
cached values are read-only after the first item's setup.
|
|
22
|
+
Function-scoped FixtureDefs get independent copies with fresh
|
|
23
|
+
cached_result and _finalizers, allowing concurrent fixture setup
|
|
24
|
+
across items without racing on shared singleton state.
|
|
25
|
+
"""
|
|
26
|
+
# noinspection PyProtectedMember
|
|
27
|
+
# request._arg2fixturedefs, fixturedef._scope: no public API;
|
|
28
|
+
# mirrors pytest's own TopRequest/FixtureDef internals.
|
|
29
|
+
request = getattr(item, "_request", None)
|
|
30
|
+
if not request or not hasattr(request, "_arg2fixturedefs"):
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
new_arg2fds = {}
|
|
34
|
+
for argname, fds in request._arg2fixturedefs.items(): # pyright: ignore[reportPrivateUsage]
|
|
35
|
+
new_fds = []
|
|
36
|
+
for fd in fds:
|
|
37
|
+
if fd._scope is Scope.Function: # pyright: ignore[reportPrivateUsage]
|
|
38
|
+
clone = FixtureDef.__new__(FixtureDef)
|
|
39
|
+
clone.__dict__.update(fd.__dict__)
|
|
40
|
+
clone.cached_result = None
|
|
41
|
+
clone._finalizers = [] # pyright: ignore[reportPrivateUsage, reportAttributeAccessIssue]
|
|
42
|
+
new_fds.append(clone)
|
|
43
|
+
else:
|
|
44
|
+
new_fds.append(fd)
|
|
45
|
+
new_arg2fds[argname] = new_fds
|
|
46
|
+
|
|
47
|
+
# _arg2fixturedefs is typed Final but not enforced at runtime
|
|
48
|
+
object.__setattr__(request, "_arg2fixturedefs", new_arg2fds)
|
|
49
|
+
|
|
15
50
|
@staticmethod
|
|
16
51
|
def save_and_clear_function_fixtures(item) -> list:
|
|
17
52
|
"""After an item's setup, save its function-scoped fixture finalizers
|
|
@@ -58,6 +93,39 @@ class FixtureManager:
|
|
|
58
93
|
if fixturedef._scope is Scope.Function and fixturedef.cached_result is not None: # pyright: ignore[reportPrivateUsage]
|
|
59
94
|
fixturedef.cached_result = None
|
|
60
95
|
|
|
96
|
+
@staticmethod
|
|
97
|
+
def populate_shared_fixtures(item) -> None:
|
|
98
|
+
"""Resolve only non-function-scoped fixtures for the item.
|
|
99
|
+
|
|
100
|
+
Populates shared fixture caches (module/class/session scope) by
|
|
101
|
+
calling getfixturevalue for each non-function-scoped fixture.
|
|
102
|
+
Function-scoped fixtures are skipped — they will be created later
|
|
103
|
+
from cloned FixtureDefs in parallel workers.
|
|
104
|
+
|
|
105
|
+
Must be called inside a setup hook context (item in setupstate)
|
|
106
|
+
so that addfinalizer works for the resolved fixtures.
|
|
107
|
+
"""
|
|
108
|
+
# noinspection PyProtectedMember
|
|
109
|
+
# request._arg2fixturedefs, fixturedef._scope: no public API;
|
|
110
|
+
# mirrors pytest's own TopRequest/FixtureDef internals.
|
|
111
|
+
request = getattr(item, "_request", None)
|
|
112
|
+
if not request or not hasattr(request, "_arg2fixturedefs"):
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
for argname in item.fixturenames:
|
|
116
|
+
if argname in item.funcargs:
|
|
117
|
+
continue
|
|
118
|
+
fds = request._arg2fixturedefs.get(argname, []) # pyright: ignore[reportPrivateUsage]
|
|
119
|
+
if fds and fds[-1]._scope is not Scope.Function: # pyright: ignore[reportPrivateUsage]
|
|
120
|
+
value = request.getfixturevalue(argname)
|
|
121
|
+
item.funcargs[argname] = value
|
|
122
|
+
# Eagerly initialize lazy state on shared fixture values.
|
|
123
|
+
# TmpPathFactory.getbasetemp() lazily creates the basetemp
|
|
124
|
+
# directory; without this, parallel workers would race on
|
|
125
|
+
# the first tmp_path_factory.mktemp() call.
|
|
126
|
+
if hasattr(value, "getbasetemp"):
|
|
127
|
+
value.getbasetemp()
|
|
128
|
+
|
|
61
129
|
@staticmethod
|
|
62
130
|
def save_collector_finalizers(session, next_item) -> list:
|
|
63
131
|
"""Save finalizers from stack nodes that would be torn down when
|