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.
Files changed (112) hide show
  1. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/.github/workflows/ci.yml +5 -0
  2. pytest_threadpool-0.3.3/.github/workflows/publish.yml +25 -0
  3. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/.gitignore +3 -0
  4. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/CONTRIBUTING.md +36 -21
  5. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/PKG-INFO +25 -6
  6. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/README.md +23 -4
  7. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/pyproject.toml +12 -4
  8. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_fixtures.py +68 -0
  9. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_runner.py +219 -62
  10. pytest_threadpool-0.3.3/src/pytest_threadpool/_version.py +34 -0
  11. pytest_threadpool-0.3.3/tests/integration_tests/cases/collected_count.py +23 -0
  12. pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_addfinalizer.py +49 -0
  13. pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_chain.py +54 -0
  14. pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_multiple_per_test.py +65 -0
  15. pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_setup_parallel.py +51 -0
  16. pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_with_shared_deps.py +61 -0
  17. pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_with_tmp_path.py +45 -0
  18. pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_with_xunit.py +55 -0
  19. pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_yield_teardown.py +47 -0
  20. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_after_test_failure.py +38 -0
  21. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_parallel_timing.py +55 -0
  22. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_same_thread.py +50 -0
  23. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_xunit_function.py +51 -0
  24. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_xunit_method_thread.py +49 -0
  25. pytest_threadpool-0.3.3/tests/integration_tests/cases/xunit_method_teardown_runs.py +43 -0
  26. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/conftest.py +9 -0
  27. pytest_threadpool-0.3.3/tests/integration_tests/test_pytester_func_fixtures.py +69 -0
  28. pytest_threadpool-0.3.3/tests/integration_tests/test_pytester_parallel_teardown.py +42 -0
  29. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_reporting.py +9 -0
  30. pytest_threadpool-0.2.0/.claude/settings.local.json +0 -30
  31. pytest_threadpool-0.2.0/ANALYSIS.md +0 -142
  32. pytest_threadpool-0.2.0/CLAUDE.md +0 -0
  33. pytest_threadpool-0.2.0/PLAN.md +0 -222
  34. pytest_threadpool-0.2.0/TODO.md +0 -44
  35. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/LICENSE +0 -0
  36. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/hooks/pre-commit +0 -0
  37. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/scripts/setup-dev +0 -0
  38. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/__init__.py +0 -0
  39. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_api.py +0 -0
  40. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_constants.py +0 -0
  41. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_grouping.py +0 -0
  42. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_markers.py +0 -0
  43. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/plugin.py +0 -0
  44. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/__init__.py +0 -0
  45. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/__init__.py +0 -0
  46. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/__init__.py +0 -0
  47. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/_templates.py +0 -0
  48. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
  49. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/class_single_method.py +0 -0
  50. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/class_thread_verification.py +0 -0
  51. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
  52. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
  53. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
  54. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_sigint.py +0 -0
  55. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
  56. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_system_exit.py +0 -0
  57. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
  58. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
  59. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
  60. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
  61. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
  62. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
  63. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
  64. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
  65. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
  66. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/marks_custom.py +0 -0
  67. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/marks_standard.py +0 -0
  68. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
  69. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
  70. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_incremental.py +0 -0
  71. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
  72. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_package_children.py +0 -0
  73. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_all_merged.py +0 -0
  74. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
  75. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
  76. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
  77. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
  78. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_module_children.py +0 -0
  79. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
  80. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
  81. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_parameters.py +0 -0
  82. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
  83. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
  84. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/setup_all_fail.py +0 -0
  85. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
  86. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_counter.py +0 -0
  87. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
  88. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
  89. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
  90. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
  91. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/validate_threadpool.py +0 -0
  92. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
  93. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
  94. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
  95. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
  96. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
  97. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
  98. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_class.py +0 -0
  99. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_edge_cases.py +0 -0
  100. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_fixtures.py +0 -0
  101. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_marks.py +0 -0
  102. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_package.py +0 -0
  103. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_scopes.py +0 -0
  104. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_sequential.py +0 -0
  105. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_shared.py +0 -0
  106. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_xunit.py +0 -0
  107. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/__init__.py +0 -0
  108. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_api.py +0 -0
  109. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_fixtures.py +0 -0
  110. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_grouping.py +0 -0
  111. {pytest_threadpool-0.2.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_markers.py +0 -0
  112. {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
@@ -163,3 +163,6 @@ cython_debug/
163
163
  .idea/
164
164
  uv.lock
165
165
 
166
+ # hatch-vcs generated version file
167
+ src/pytest_threadpool/_version.py
168
+
@@ -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
- log = {}
46
- barrier = threading.Barrier(3, timeout=10)
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
- ftdir.makepyfile("""
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
- result = ftdir.run_pytest("--threadpool", "2")
81
- result.assert_outcomes(passed=2)
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
- barrier = threading.Barrier(2, timeout=10)
86
- def test_a(self): self.barrier.wait()
87
- def test_b(self): self.barrier.wait()
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 | Scope |
132
- |-----|-------|
133
- | `[Runner]` | Core parallel execution engine (`_runner.py`, `_fixtures.py`) |
134
- | `[Markers]` | Marker resolution and grouping (`_markers.py`, `_grouping.py`, `_constants.py`) |
135
- | `[Report]` | Terminal reporting and display (`_LiveReporter`) |
136
- | `[Plugin]` | Plugin hooks and CLI options (`plugin.py`) |
137
- | `[API]` | Public API changes (`_api.py`, `__init__.py`) |
138
- | `[Test]` | Test additions or changes only |
139
- | `[Tooling]` | Ruff, pyright, pre-commit, CI, scripts |
140
- | `[Docs]` | README, CONTRIBUTING, docstrings |
141
- | `[Refactor]` | Internal restructuring, no behavior change |
142
- | `[Fix]` | Bug fix must include issue number if fixing a known issue |
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.2.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
- [![PyPI](https://img.shields.io/pypi/v/pytest-threadpool.svg)](https://pypi.org/project/pytest-threadpool/)
21
- [![Python](https://img.shields.io/pypi/pyversions/pytest-threadpool.svg)](https://pypi.org/project/pytest-threadpool/)
20
+ [![PyPI - Version](https://img.shields.io/pypi/v/pytest-threadpool)](https://pypi.org/project/pytest-threadpool/)
21
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytest-threadpool)](https://pypi.org/project/pytest-threadpool/)
22
22
  [![License](https://img.shields.io/github/license/pytest-threadpool/pytest-threadpool)](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
23
23
  [![GitHub](https://img.shields.io/badge/GitHub-pytest--threadpool-blue?logo=github)](https://github.com/pytest-threadpool/pytest-threadpool)
24
24
  [![CI](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml/badge.svg)](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* concurrently in a `ThreadPoolExecutor` while keeping
30
- fixture setup/teardown sequential (pytest internals are not thread-safe).
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
- [![PyPI](https://img.shields.io/pypi/v/pytest-threadpool.svg)](https://pypi.org/project/pytest-threadpool/)
4
- [![Python](https://img.shields.io/pypi/pyversions/pytest-threadpool.svg)](https://pypi.org/project/pytest-threadpool/)
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/pytest-threadpool)](https://pypi.org/project/pytest-threadpool/)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytest-threadpool)](https://pypi.org/project/pytest-threadpool/)
5
5
  [![License](https://img.shields.io/github/license/pytest-threadpool/pytest-threadpool)](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
6
6
  [![GitHub](https://img.shields.io/badge/GitHub-pytest--threadpool-blue?logo=github)](https://github.com/pytest-threadpool/pytest-threadpool)
7
7
  [![CI](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml/badge.svg)](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* concurrently in a `ThreadPoolExecutor` while keeping
13
- fixture setup/teardown sequential (pytest internals are not thread-safe).
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
- version = "0.2.0"
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