pytest-threadpool 0.3.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.3.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.3.0 → pytest_threadpool-0.3.3}/.gitignore +3 -0
  4. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/CONTRIBUTING.md +35 -20
  5. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/PKG-INFO +11 -12
  6. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/README.md +10 -10
  7. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/pyproject.toml +11 -4
  8. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_runner.py +72 -32
  9. pytest_threadpool-0.3.3/src/pytest_threadpool/_version.py +34 -0
  10. pytest_threadpool-0.3.3/tests/integration_tests/cases/collected_count.py +23 -0
  11. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_after_test_failure.py +38 -0
  12. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_parallel_timing.py +55 -0
  13. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_same_thread.py +50 -0
  14. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_xunit_function.py +51 -0
  15. pytest_threadpool-0.3.3/tests/integration_tests/cases/teardown_xunit_method_thread.py +49 -0
  16. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/conftest.py +9 -0
  17. pytest_threadpool-0.3.3/tests/integration_tests/test_pytester_parallel_teardown.py +42 -0
  18. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_reporting.py +9 -0
  19. pytest_threadpool-0.3.0/.claude/settings.local.json +0 -30
  20. pytest_threadpool-0.3.0/ANALYSIS.md +0 -142
  21. pytest_threadpool-0.3.0/CLAUDE.md +0 -0
  22. pytest_threadpool-0.3.0/PLAN.md +0 -223
  23. pytest_threadpool-0.3.0/TODO.md +0 -32
  24. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/LICENSE +0 -0
  25. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/hooks/pre-commit +0 -0
  26. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/scripts/setup-dev +0 -0
  27. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/__init__.py +0 -0
  28. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_api.py +0 -0
  29. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_constants.py +0 -0
  30. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_fixtures.py +0 -0
  31. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_grouping.py +0 -0
  32. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/_markers.py +0 -0
  33. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/src/pytest_threadpool/plugin.py +0 -0
  34. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/__init__.py +0 -0
  35. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/__init__.py +0 -0
  36. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/__init__.py +0 -0
  37. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/_templates.py +0 -0
  38. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
  39. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/class_single_method.py +0 -0
  40. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/class_thread_verification.py +0 -0
  41. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
  42. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
  43. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
  44. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_sigint.py +0 -0
  45. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
  46. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/edge_system_exit.py +0 -0
  47. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
  48. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
  49. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
  50. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_func_addfinalizer.py +0 -0
  51. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_func_chain.py +0 -0
  52. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_func_multiple_per_test.py +0 -0
  53. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_func_setup_parallel.py +0 -0
  54. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_func_with_shared_deps.py +0 -0
  55. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_func_with_tmp_path.py +0 -0
  56. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_func_with_xunit.py +0 -0
  57. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_func_yield_teardown.py +0 -0
  58. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
  59. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
  60. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
  61. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
  62. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
  63. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
  64. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/marks_custom.py +0 -0
  65. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/marks_standard.py +0 -0
  66. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
  67. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
  68. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_incremental.py +0 -0
  69. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
  70. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/reporting_package_children.py +0 -0
  71. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_all_merged.py +0 -0
  72. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
  73. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
  74. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
  75. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
  76. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_module_children.py +0 -0
  77. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
  78. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
  79. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/scope_parameters.py +0 -0
  80. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
  81. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
  82. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/setup_all_fail.py +0 -0
  83. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
  84. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_counter.py +0 -0
  85. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
  86. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
  87. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
  88. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
  89. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/validate_threadpool.py +0 -0
  90. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
  91. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
  92. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
  93. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
  94. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
  95. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_method_teardown_runs.py +0 -0
  96. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
  97. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_class.py +0 -0
  98. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_edge_cases.py +0 -0
  99. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_fixtures.py +0 -0
  100. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_func_fixtures.py +0 -0
  101. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_marks.py +0 -0
  102. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_package.py +0 -0
  103. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_scopes.py +0 -0
  104. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_sequential.py +0 -0
  105. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_shared.py +0 -0
  106. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/integration_tests/test_pytester_xunit.py +0 -0
  107. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/unit_tests/__init__.py +0 -0
  108. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_api.py +0 -0
  109. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_fixtures.py +0 -0
  110. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_grouping.py +0 -0
  111. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.3}/tests/unit_tests/test_unit_markers.py +0 -0
  112. {pytest_threadpool-0.3.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
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-threadpool
3
- Version: 0.3.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
@@ -13,13 +13,12 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Topic :: Software Development :: Testing
14
14
  Requires-Python: >=3.13
15
15
  Requires-Dist: pytest>=9.0.2
16
- Requires-Dist: selenium>=4.41.0
17
16
  Description-Content-Type: text/markdown
18
17
 
19
18
  # pytest-threadpool
20
19
 
21
- ![PyPI - Version](https://img.shields.io/pypi/v/pytest-threadpool)
22
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/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/)
23
22
  [![License](https://img.shields.io/github/license/pytest-threadpool/pytest-threadpool)](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
24
23
  [![GitHub](https://img.shields.io/badge/GitHub-pytest--threadpool-blue?logo=github)](https://github.com/pytest-threadpool/pytest-threadpool)
25
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)
@@ -27,9 +26,9 @@ Description-Content-Type: text/markdown
27
26
 
28
27
  **Status: Beta** · Parallel test execution for free-threaded Python builds (3.13t+).
29
28
 
30
- Runs test *bodies* and function-scoped fixture setup concurrently in a thread
31
- pool while keeping shared fixtures (module/class/session scope) and teardown
32
- sequential.
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.
33
32
 
34
33
  ## Installation
35
34
 
@@ -88,12 +87,12 @@ sequentially before workers launch and served from cache to all items.
88
87
 
89
88
  | Scope | Behavior |
90
89
  |------------|-------------------------------------------------|
91
- | `function` | Cloned per-item, set up in parallel workers |
92
- | `class` | Resolved once, cached, shared across items |
93
- | `module` | Resolved once, cached, shared across items |
94
- | `session` | Resolved once, cached, shared across items |
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 |
95
94
 
96
- Teardown for all scopes runs sequentially after the parallel group completes.
95
+ Shared fixture teardown runs sequentially after the parallel group completes.
97
96
 
98
97
  ## Marker levels
99
98
 
@@ -1,7 +1,7 @@
1
1
  # pytest-threadpool
2
2
 
3
- ![PyPI - Version](https://img.shields.io/pypi/v/pytest-threadpool)
4
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/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,9 +9,9 @@
9
9
 
10
10
  **Status: Beta** · Parallel test execution for free-threaded Python builds (3.13t+).
11
11
 
12
- Runs test *bodies* and function-scoped fixture setup concurrently in a thread
13
- pool while keeping shared fixtures (module/class/session scope) and teardown
14
- sequential.
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.
15
15
 
16
16
  ## Installation
17
17
 
@@ -70,12 +70,12 @@ sequentially before workers launch and served from cache to all items.
70
70
 
71
71
  | Scope | Behavior |
72
72
  |------------|-------------------------------------------------|
73
- | `function` | Cloned per-item, set up 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 |
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
77
 
78
- Teardown for all scopes runs sequentially after the parallel group completes.
78
+ Shared fixture teardown runs sequentially after the parallel group completes.
79
79
 
80
80
  ## Marker levels
81
81
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pytest-threadpool"
3
- version = "0.3.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"
@@ -16,7 +16,6 @@ classifiers = [
16
16
  ]
17
17
  dependencies = [
18
18
  "pytest>=9.0.2",
19
- "selenium>=4.41.0",
20
19
  ]
21
20
 
22
21
  [project.entry-points.pytest11]
@@ -30,24 +29,32 @@ dev = [
30
29
  ]
31
30
 
32
31
  [build-system]
33
- requires = ["hatchling"]
32
+ requires = ["hatchling", "hatch-vcs"]
34
33
  build-backend = "hatchling.build"
35
34
 
35
+ [tool.hatch.version]
36
+ source = "vcs"
37
+
38
+ [tool.hatch.build.hooks.vcs]
39
+ version-file = "src/pytest_threadpool/_version.py"
40
+
36
41
  [tool.hatch.build.targets.wheel]
37
42
  packages = ["src/pytest_threadpool"]
38
43
 
39
44
  [tool.pytest.ini_options]
40
45
  addopts = ["--threadpool", "auto"]
46
+ norecursedirs = ["cases"]
41
47
 
42
48
  [tool.pyright]
43
49
  pythonVersion = "3.13"
44
50
  include = ["src", "tests"]
45
51
  typeCheckingMode = "standard"
46
52
  reportPrivateUsage = false
47
- ignore = ["tests/unit_tests"]
53
+ ignore = ["tests/unit_tests", "src/pytest_threadpool/_version.py"]
48
54
 
49
55
  [tool.ruff]
50
56
  target-version = "py313"
57
+ exclude = ["src/pytest_threadpool/_version.py"]
51
58
  src = ["src", "tests"]
52
59
  line-length = 99
53
60
 
@@ -394,7 +394,7 @@ class ParallelRunner:
394
394
  ihook.pytest_runtest_logreport(report=setup_reports[item])
395
395
  if setup_passed[item] and session.config.getoption("setupshow", False):
396
396
  show_test_item(item)
397
- self._teardown_all(items, per_item_fixture_fins, saved_collector_fins)
397
+ self._teardown_all(items, per_item_fixture_fins, {}, saved_collector_fins)
398
398
  return
399
399
 
400
400
  # Phase 1+2: fire hooks for all items, populate shared fixture caches,
@@ -470,8 +470,8 @@ class ParallelRunner:
470
470
 
471
471
  cancelled = threading.Event()
472
472
 
473
- def _do_setup_and_call(test_item):
474
- """Setup + call worker (for items with cloned FixtureDefs).
473
+ def _do_setup_call_teardown(test_item):
474
+ """Setup + call + teardown worker (for items with cloned FixtureDefs).
475
475
 
476
476
  Fixture setup uses cloned function-scoped FixtureDefs so each
477
477
  worker creates independent fixture instances without racing
@@ -479,13 +479,17 @@ class ParallelRunner:
479
479
  from the first item's sequential setup, so their FixtureDef.execute()
480
480
  is a cache-hit read.
481
481
 
482
+ Function-scoped fixture teardown (yield cleanup, addfinalizer
483
+ callbacks, xunit teardown_method) also runs in the worker since
484
+ each item's finalizers are independent.
485
+
482
486
  addfinalizer is redirected to a per-item list to avoid the
483
487
  setupstate stack assertion (the item IS in the stack, but
484
488
  list.append on the stack entry is also safe on free-threaded Python;
485
489
  the redirect simply avoids coupling to setupstate internals).
486
490
  """
487
491
  if cancelled.is_set():
488
- return test_item, None, None, []
492
+ return test_item, None, None, None
489
493
 
490
494
  # Redirect node.addfinalizer to per-item list
491
495
  original_addfinalizer = test_item.addfinalizer
@@ -499,16 +503,35 @@ class ParallelRunner:
499
503
 
500
504
  if setup_info.excinfo is None:
501
505
  fixture_fins = FixtureManager.save_and_clear_function_fixtures(test_item)
506
+ call_info = None
502
507
  if not cancelled.is_set():
503
508
  live.mark_running(test_item)
504
509
  call_info = CallInfo.from_call(lambda: test_item.runtest(), when="call")
505
510
  if not cancelled.is_set():
506
511
  live.mark_call_done(test_item, call_info.excinfo)
507
- return test_item, setup_info, call_info, fixture_fins
508
- return test_item, setup_info, None, fixture_fins
512
+
513
+ # Run function-scoped fixture teardown in the worker.
514
+ # Includes yield cleanup, addfinalizer callbacks, and
515
+ # node-level finalizers captured during setup.
516
+ all_fins = list(node_fins) + fixture_fins
517
+
518
+ def _run_fins(fns=all_fins):
519
+ exceptions = []
520
+ for fn in reversed(fns):
521
+ try:
522
+ fn()
523
+ except BaseException as e:
524
+ exceptions.append(e)
525
+ if len(exceptions) == 1:
526
+ raise exceptions[0]
527
+ if exceptions:
528
+ raise BaseExceptionGroup("errors during fixture teardown", exceptions)
529
+
530
+ teardown_info = CallInfo.from_call(_run_fins, when="teardown")
531
+ return test_item, setup_info, call_info, teardown_info
509
532
 
510
533
  FixtureManager.clear_function_fixture_caches(test_item)
511
- return test_item, setup_info, None, []
534
+ return test_item, setup_info, None, None
512
535
 
513
536
  def _report_item(item):
514
537
  """Report a single item — live cursor mode or plain fallback."""
@@ -536,6 +559,8 @@ class ParallelRunner:
536
559
 
537
560
  reported.add(item)
538
561
 
562
+ teardown_infos = {}
563
+
539
564
  interrupted = False
540
565
  if workers > 1 and len(parallel_items) > 1:
541
566
  work_queue = queue.SimpleQueue()
@@ -547,14 +572,15 @@ class ParallelRunner:
547
572
  if work_item is None or cancelled.is_set():
548
573
  return
549
574
  try:
550
- item, setup_info, call_info, fixture_fins = _do_setup_and_call(work_item)
551
- per_item_fixture_fins[item] = fixture_fins
575
+ item, setup_info, call_info, td_info = _do_setup_call_teardown(work_item)
552
576
  if setup_info is not None:
553
577
  setup_rep = item.ihook.pytest_runtest_makereport(
554
578
  item=item, call=setup_info
555
579
  )
556
580
  setup_reports[item] = setup_rep
557
581
  setup_passed[item] = setup_info.excinfo is None
582
+ if td_info is not None:
583
+ teardown_infos[item] = td_info
558
584
  result_queue.put((item, call_info))
559
585
  except BaseException as exc:
560
586
  call_info = CallInfo.from_call(
@@ -569,7 +595,7 @@ class ParallelRunner:
569
595
  t.start()
570
596
  threads.append(t)
571
597
 
572
- # Submit all parallel items (setup + call)
598
+ # Submit all parallel items (setup + call + teardown)
573
599
  for item in parallel_items:
574
600
  work_queue.put(item)
575
601
 
@@ -591,17 +617,18 @@ class ParallelRunner:
591
617
  for t in threads:
592
618
  t.join()
593
619
  else:
594
- # Single worker fallback: sequential setup + call
620
+ # Single worker fallback
595
621
  try:
596
622
  for item in parallel_items:
597
- item, setup_info, call_info, fixture_fins = _do_setup_and_call(item)
598
- per_item_fixture_fins[item] = fixture_fins
623
+ item, setup_info, call_info, td_info = _do_setup_call_teardown(item)
599
624
  if setup_info is not None:
600
625
  setup_rep = item.ihook.pytest_runtest_makereport(
601
626
  item=item, call=setup_info
602
627
  )
603
628
  setup_reports[item] = setup_rep
604
629
  setup_passed[item] = setup_info.excinfo is None
630
+ if td_info is not None:
631
+ teardown_infos[item] = td_info
605
632
  if call_info is not None:
606
633
  call_results[item] = call_info
607
634
  _report_item(item)
@@ -622,33 +649,46 @@ class ParallelRunner:
622
649
  if item in session._setupstate.stack: # pyright: ignore[reportPrivateUsage]
623
650
  session._setupstate.stack.pop(item) # pyright: ignore[reportPrivateUsage]
624
651
 
625
- # Teardown (always runs, even after interrupt)
626
- self._teardown_all(items, per_item_fixture_fins, saved_collector_fins)
652
+ # Teardown reporting + collector teardown (always runs, even after interrupt)
653
+ self._teardown_all(items, per_item_fixture_fins, teardown_infos, saved_collector_fins)
627
654
 
628
655
  if interrupted:
629
656
  raise KeyboardInterrupt
630
657
 
631
- def _teardown_all(self, items, per_item_fixture_fins, saved_collector_fins) -> None:
632
- """Run per-item function-level finalizers, saved collector finalizers,
633
- then tear down remaining collectors."""
658
+ def _teardown_all(
659
+ self, items, per_item_fixture_fins, teardown_infos, saved_collector_fins
660
+ ) -> None:
661
+ """Report teardown results, run any remaining finalizers, and tear down
662
+ collectors.
663
+
664
+ For items with pre-computed teardown_infos (from parallel workers),
665
+ only reporting happens here. For items without (setuponly mode),
666
+ finalizers from per_item_fixture_fins are executed sequentially.
667
+ """
634
668
  session = self._session
635
669
 
636
670
  for item in items:
637
- fins = per_item_fixture_fins.get(item, [])
671
+ if item in teardown_infos:
672
+ # Teardown already ran in the worker — just report.
673
+ teardown_info = teardown_infos[item]
674
+ else:
675
+ # Fallback: run finalizers now (setuponly mode).
676
+ fins = per_item_fixture_fins.get(item, [])
677
+
678
+ def _run_fins(fns=fins):
679
+ exceptions = []
680
+ for fn in reversed(fns):
681
+ try:
682
+ fn()
683
+ except BaseException as e:
684
+ exceptions.append(e)
685
+ if len(exceptions) == 1:
686
+ raise exceptions[0]
687
+ if exceptions:
688
+ raise BaseExceptionGroup("errors during fixture teardown", exceptions)
689
+
690
+ teardown_info = CallInfo.from_call(_run_fins, when="teardown")
638
691
 
639
- def _run_fins(fns=fins):
640
- exceptions = []
641
- for fn in reversed(fns):
642
- try:
643
- fn()
644
- except BaseException as e:
645
- exceptions.append(e)
646
- if len(exceptions) == 1:
647
- raise exceptions[0]
648
- if exceptions:
649
- raise BaseExceptionGroup("errors during fixture teardown", exceptions)
650
-
651
- teardown_info = CallInfo.from_call(_run_fins, when="teardown")
652
692
  rep = item.ihook.pytest_runtest_makereport(item=item, call=teardown_info)
653
693
  item.ihook.pytest_runtest_logreport(report=rep)
654
694
 
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.3.3'
32
+ __version_tuple__ = version_tuple = (0, 3, 3)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,23 @@
1
+ """Verify collected count matches actual test count (not inflated by cloning)."""
2
+
3
+ import threading
4
+
5
+ import pytest
6
+
7
+
8
+ @pytest.mark.parallelizable("children")
9
+ class TestExactCount:
10
+ barrier = threading.Barrier(3, timeout=10)
11
+
12
+ def test_a(self):
13
+ self.barrier.wait()
14
+
15
+ def test_b(self):
16
+ self.barrier.wait()
17
+
18
+ def test_c(self):
19
+ self.barrier.wait()
20
+
21
+
22
+ def test_standalone():
23
+ assert True
@@ -0,0 +1,38 @@
1
+ """Teardown runs even when the test call fails."""
2
+
3
+ import threading
4
+ from typing import ClassVar
5
+
6
+ import pytest
7
+
8
+
9
+ class TestState:
10
+ teardown_log: ClassVar[list] = []
11
+ lock: ClassVar[threading.Lock] = threading.Lock()
12
+
13
+
14
+ @pytest.fixture
15
+ def resource_with_cleanup(request):
16
+ name = request.node.name
17
+ yield name
18
+ with TestState.lock:
19
+ TestState.teardown_log.append(name)
20
+
21
+
22
+ @pytest.mark.parallelizable("children")
23
+ class TestTeardownAfterFailure:
24
+ def test_pass(self, resource_with_cleanup):
25
+ assert True
26
+
27
+ def test_fail(self, resource_with_cleanup):
28
+ pytest.fail("intentional failure")
29
+
30
+ def test_raise(self, resource_with_cleanup):
31
+ raise RuntimeError("intentional")
32
+
33
+
34
+ def test_verify():
35
+ expected = {"test_pass", "test_fail", "test_raise"}
36
+ assert expected == set(TestState.teardown_log), (
37
+ f"teardown must run for all tests including failures: {TestState.teardown_log}"
38
+ )
@@ -0,0 +1,55 @@
1
+ """Verify function-scoped fixture teardown runs in parallel, not sequentially."""
2
+
3
+ import threading
4
+ import time
5
+ from typing import ClassVar
6
+
7
+ import pytest
8
+
9
+
10
+ class TestState:
11
+ teardown_end_times: ClassVar[list] = []
12
+ call_end_times: ClassVar[list] = []
13
+ lock: ClassVar[threading.Lock] = threading.Lock()
14
+
15
+
16
+ @pytest.fixture
17
+ def slow_teardown_resource():
18
+ yield "value"
19
+ # Each teardown takes 0.2s; if sequential that's 0.6s total
20
+ time.sleep(0.2)
21
+ with TestState.lock:
22
+ TestState.teardown_end_times.append(time.monotonic())
23
+
24
+
25
+ @pytest.mark.parallelizable("children")
26
+ class TestParallelTeardown:
27
+ barrier = threading.Barrier(3, timeout=10)
28
+
29
+ def _record_call(self):
30
+ with TestState.lock:
31
+ TestState.call_end_times.append(time.monotonic())
32
+
33
+ def test_a(self, slow_teardown_resource):
34
+ self.barrier.wait()
35
+ self._record_call()
36
+
37
+ def test_b(self, slow_teardown_resource):
38
+ self.barrier.wait()
39
+ self._record_call()
40
+
41
+ def test_c(self, slow_teardown_resource):
42
+ self.barrier.wait()
43
+ self._record_call()
44
+
45
+
46
+ def test_verify():
47
+ assert len(TestState.teardown_end_times) == 3
48
+ assert len(TestState.call_end_times) == 3
49
+ # Measure from when tests finished to when all teardowns completed
50
+ teardown_duration = max(TestState.teardown_end_times) - max(TestState.call_end_times)
51
+ # 3 x 0.2s sequential = 0.6s; parallel should complete in ~0.2s + overhead
52
+ assert teardown_duration < 0.45, (
53
+ f"Teardown appears sequential: {teardown_duration:.2f}s "
54
+ f"(3 x 0.2s = 0.6s sequential, expected < 0.45s parallel)"
55
+ )