pytest-threadpool 0.3.0__tar.gz → 0.3.4__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 (143) hide show
  1. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/.github/workflows/ci.yml +7 -1
  2. pytest_threadpool-0.3.4/.github/workflows/publish.yml +25 -0
  3. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/.gitignore +3 -0
  4. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/CONTRIBUTING.md +38 -21
  5. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/PKG-INFO +23 -14
  6. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/README.md +19 -12
  7. pytest_threadpool-0.3.4/codecov.yml +11 -0
  8. pytest_threadpool-0.3.4/examples/__init__.py +3 -0
  9. pytest_threadpool-0.3.4/examples/test_di/__init__.py +1 -0
  10. pytest_threadpool-0.3.4/examples/test_di/conftest.py +28 -0
  11. pytest_threadpool-0.3.4/examples/test_di/container.py +44 -0
  12. pytest_threadpool-0.3.4/examples/test_di/providers.py +104 -0
  13. pytest_threadpool-0.3.4/examples/test_di/services.py +53 -0
  14. pytest_threadpool-0.3.4/examples/test_di/test_factory.py +19 -0
  15. pytest_threadpool-0.3.4/examples/test_di/test_local.py +99 -0
  16. pytest_threadpool-0.3.4/examples/test_di/test_singleton.py +20 -0
  17. pytest_threadpool-0.3.4/examples/test_di/test_thread_local.py +77 -0
  18. pytest_threadpool-0.3.4/examples/test_logging/test_file_logging.py +30 -0
  19. pytest_threadpool-0.3.4/examples/test_logging/test_log_capture.py +66 -0
  20. pytest_threadpool-0.3.4/examples/test_logging/test_print_capture.py +44 -0
  21. pytest_threadpool-0.3.4/examples/test_queue/conftest.py +16 -0
  22. pytest_threadpool-0.3.4/examples/test_queue/test_queue.py +30 -0
  23. pytest_threadpool-0.3.4/examples/test_queue/user_pool.py +26 -0
  24. pytest_threadpool-0.3.4/examples/test_shared_state/test_barrier.py +47 -0
  25. pytest_threadpool-0.3.4/examples/test_shared_state/test_counter.py +43 -0
  26. pytest_threadpool-0.3.4/examples/test_shared_state/test_counter_async.py +42 -0
  27. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/hooks/pre-commit +2 -2
  28. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/pyproject.toml +18 -4
  29. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_grouping.py +50 -2
  30. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_markers.py +23 -0
  31. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_runner.py +270 -59
  32. pytest_threadpool-0.3.4/src/pytest_threadpool/_version.py +34 -0
  33. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/plugin.py +5 -2
  34. pytest_threadpool-0.3.4/tests/__init__.py +0 -0
  35. pytest_threadpool-0.3.4/tests/integration_tests/cases/__init__.py +0 -0
  36. pytest_threadpool-0.3.4/tests/integration_tests/cases/capture_print_parallel.py +13 -0
  37. pytest_threadpool-0.3.4/tests/integration_tests/cases/capture_two_groups.py +30 -0
  38. pytest_threadpool-0.3.4/tests/integration_tests/cases/collected_count.py +23 -0
  39. pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_from_conftest.py +38 -0
  40. pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_from_conftest_conftest.py +9 -0
  41. pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_implicit_scope.py +56 -0
  42. pytest_threadpool-0.3.4/tests/integration_tests/cases/reporting_single_file_params.py +12 -0
  43. pytest_threadpool-0.3.4/tests/integration_tests/cases/reporting_stdout_during_parallel.py +13 -0
  44. pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_after_test_failure.py +38 -0
  45. pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_parallel_timing.py +55 -0
  46. pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_same_thread.py +50 -0
  47. pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_xunit_function.py +51 -0
  48. pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_xunit_method_thread.py +49 -0
  49. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/conftest.py +9 -0
  50. pytest_threadpool-0.3.4/tests/integration_tests/test_capture.py +172 -0
  51. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_edge_cases.py +5 -7
  52. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_func_fixtures.py +23 -1
  53. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_package.py +78 -0
  54. pytest_threadpool-0.3.4/tests/integration_tests/test_pytester_parallel_teardown.py +42 -0
  55. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_reporting.py +136 -0
  56. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_grouping.py +65 -0
  57. pytest_threadpool-0.3.4/tests/unit_tests/test_unit_stream_proxy.py +122 -0
  58. pytest_threadpool-0.3.0/.claude/settings.local.json +0 -30
  59. pytest_threadpool-0.3.0/ANALYSIS.md +0 -142
  60. pytest_threadpool-0.3.0/PLAN.md +0 -223
  61. pytest_threadpool-0.3.0/TODO.md +0 -32
  62. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/LICENSE +0 -0
  63. {pytest_threadpool-0.3.0/tests → pytest_threadpool-0.3.4/examples/test_logging}/__init__.py +0 -0
  64. {pytest_threadpool-0.3.0/tests/integration_tests/cases → pytest_threadpool-0.3.4/examples/test_queue}/__init__.py +0 -0
  65. /pytest_threadpool-0.3.0/CLAUDE.md → /pytest_threadpool-0.3.4/examples/test_shared_state/__init__.py +0 -0
  66. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/scripts/setup-dev +0 -0
  67. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/__init__.py +0 -0
  68. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_api.py +0 -0
  69. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_constants.py +0 -0
  70. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_fixtures.py +0 -0
  71. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/__init__.py +0 -0
  72. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/_templates.py +0 -0
  73. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
  74. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/class_single_method.py +0 -0
  75. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/class_thread_verification.py +0 -0
  76. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
  77. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
  78. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
  79. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_sigint.py +0 -0
  80. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
  81. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_system_exit.py +0 -0
  82. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
  83. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
  84. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
  85. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_addfinalizer.py +0 -0
  86. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_chain.py +0 -0
  87. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_multiple_per_test.py +0 -0
  88. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_setup_parallel.py +0 -0
  89. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_with_shared_deps.py +0 -0
  90. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_with_tmp_path.py +0 -0
  91. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_with_xunit.py +0 -0
  92. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_yield_teardown.py +0 -0
  93. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
  94. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
  95. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
  96. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
  97. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
  98. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
  99. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/marks_custom.py +0 -0
  100. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/marks_standard.py +0 -0
  101. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
  102. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
  103. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_incremental.py +0 -0
  104. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
  105. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_package_children.py +0 -0
  106. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_all_merged.py +0 -0
  107. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
  108. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
  109. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
  110. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
  111. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_module_children.py +0 -0
  112. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
  113. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
  114. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_parameters.py +0 -0
  115. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
  116. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
  117. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/setup_all_fail.py +0 -0
  118. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
  119. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_counter.py +0 -0
  120. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
  121. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
  122. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
  123. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
  124. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/validate_threadpool.py +0 -0
  125. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
  126. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
  127. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
  128. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
  129. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
  130. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_method_teardown_runs.py +0 -0
  131. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
  132. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_class.py +0 -0
  133. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_fixtures.py +0 -0
  134. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_marks.py +0 -0
  135. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_scopes.py +0 -0
  136. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_sequential.py +0 -0
  137. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_shared.py +0 -0
  138. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_xunit.py +0 -0
  139. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/__init__.py +0 -0
  140. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_api.py +0 -0
  141. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_fixtures.py +0 -0
  142. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_markers.py +0 -0
  143. {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_plugin.py +0 -0
@@ -3,14 +3,18 @@ name: CI
3
3
  on:
4
4
  push:
5
5
  branches: [main]
6
+ tags-ignore: ["v*"]
6
7
  pull_request:
7
8
  branches: [main]
9
+ workflow_call:
8
10
 
9
11
  jobs:
10
12
  lint:
11
13
  runs-on: ubuntu-latest
12
14
  steps:
13
15
  - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
14
18
  - uses: astral-sh/setup-uv@v6
15
19
  - run: uv sync --python 3.14t --group dev
16
20
  - run: uv run ruff format --check src/ tests/
@@ -22,9 +26,11 @@ jobs:
22
26
  strategy:
23
27
  fail-fast: false
24
28
  matrix:
25
- python: ["3.13t", "3.14t", "3.15t"]
29
+ python: ["3.13", "3.13t", "3.14", "3.14t", "3.15", "3.15t"]
26
30
  steps:
27
31
  - uses: actions/checkout@v4
32
+ with:
33
+ fetch-depth: 0
28
34
  - uses: astral-sh/setup-uv@v6
29
35
  - run: uv python install ${{ matrix.python }}
30
36
  - 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
+
@@ -13,7 +13,9 @@ This installs a free-threaded Python via uv, syncs all dependencies (including
13
13
  dev tools), and sets up a pre-commit hook that runs `ruff format`, `ruff check`,
14
14
  and `pyright` before each commit.
15
15
 
16
- > Python 3.13t and 3.14t ship with the GIL disabled by default.
16
+ > Free-threaded builds (3.13t, 3.14t, 3.15t) ship with the GIL disabled and
17
+ > provide true parallelism. Standard builds (3.13, 3.14, 3.15) are also
18
+ > supported — threads work, but the GIL serializes CPU-bound code.
17
19
 
18
20
  ## Architecture rules
19
21
 
@@ -40,10 +42,13 @@ Any mutable state needed across tests or across a test run must be held as
40
42
  class attributes or instance attributes — never as module-level variables.
41
43
 
42
44
  ```python
45
+ import threading
46
+
43
47
  # Good
44
48
  class TestState:
45
- log = {}
46
- barrier = threading.Barrier(3, timeout=10)
49
+ log = {}
50
+ barrier = threading.Barrier(3, timeout=10)
51
+
47
52
 
48
53
  # Bad
49
54
  _log = {}
@@ -67,9 +72,11 @@ subprocess with the exact thread count needed. Use explicit thread counts for
67
72
  barrier-based tests (not `auto`) to avoid deadlocks on low-core machines.
68
73
 
69
74
  ```python
75
+ import threading
76
+
70
77
  # Good — isolated subprocess via ftdir, explicit thread count
71
78
  def test_children_run_concurrently(self, ftdir):
72
- ftdir.makepyfile("""
79
+ ftdir.makepyfile("""
73
80
  import threading, pytest
74
81
  @pytest.mark.parallelizable("children")
75
82
  class TestBarrier:
@@ -77,16 +84,25 @@ def test_children_run_concurrently(self, ftdir):
77
84
  def test_a(self): self.barrier.wait()
78
85
  def test_b(self): self.barrier.wait()
79
86
  """)
80
- result = ftdir.run_pytest("--threadpool", "2")
81
- result.assert_outcomes(passed=2)
87
+ result = ftdir.run_pytest("--threadpool", "2")
88
+ result.assert_outcomes(passed=2)
89
+
82
90
 
83
91
  # Bad — depends on outer runner flags
84
92
  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()
93
+ barrier = threading.Barrier(2, timeout=10)
94
+
95
+ def test_a(self): self.barrier.wait()
96
+
97
+ def test_b(self): self.barrier.wait()
88
98
  ```
89
99
 
100
+ ### Use `pytest.fail()` for intentional failures
101
+
102
+ In test cases that need to intentionally fail (e.g., verifying teardown after
103
+ failure), use `pytest.fail("reason")` instead of `assert False, "reason"`.
104
+ The latter triggers ruff PT015 and B011.
105
+
90
106
  ### Constants over strings
91
107
 
92
108
  Use `ParallelScope` enum and `_constants` module instead of bare string
@@ -128,18 +144,19 @@ Every commit message starts with one or more tags in brackets:
128
144
 
129
145
  ### Tags
130
146
 
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 |
147
+ | Tag | Scope |
148
+ |--------------|---------------------------------------------------------------------------------|
149
+ | `[Runner]` | Core parallel execution engine (`_runner.py`, `_fixtures.py`) |
150
+ | `[Markers]` | Marker resolution and grouping (`_markers.py`, `_grouping.py`, `_constants.py`) |
151
+ | `[Report]` | Terminal reporting and display (`_LiveReporter`) |
152
+ | `[Plugin]` | Plugin hooks and CLI options (`plugin.py`) |
153
+ | `[API]` | Public API changes (`_api.py`, `__init__.py`) |
154
+ | `[Test]` | Test additions or changes only |
155
+ | `[Build]` | CI/CD pipelines, publishing, versioning |
156
+ | `[Tooling]` | Ruff, pyright, pre-commit, scripts |
157
+ | `[Docs]` | README, CONTRIBUTING, docstrings |
158
+ | `[Refactor]` | Internal restructuring, no behavior change |
159
+ | `[Fix]` | Bug fix — must include issue number if fixing a known issue |
143
160
 
144
161
  ### Combining tags
145
162
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-threadpool
3
- Version: 0.3.0
3
+ Version: 0.3.4
4
4
  Summary: Parallel test execution for free-threaded Python builds
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -10,26 +10,32 @@ Classifier: Framework :: Pytest
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
13
16
  Classifier: Topic :: Software Development :: Testing
14
17
  Requires-Python: >=3.13
15
18
  Requires-Dist: pytest>=9.0.2
16
- Requires-Dist: selenium>=4.41.0
17
19
  Description-Content-Type: text/markdown
18
20
 
19
21
  # pytest-threadpool
20
22
 
21
- ![PyPI - Version](https://img.shields.io/pypi/v/pytest-threadpool)
22
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytest-threadpool)
23
+ [![PyPI - Version](https://img.shields.io/pypi/v/pytest-threadpool)](https://pypi.org/project/pytest-threadpool/)
24
+ [![Python](https://img.shields.io/badge/python-3.13%20%7C%203.13t%20%7C%203.14%20%7C%203.14t%20%7C%203.15%20%7C%203.15t-blue?logo=python&logoColor=white)](https://pypi.org/project/pytest-threadpool/)
23
25
  [![License](https://img.shields.io/github/license/pytest-threadpool/pytest-threadpool)](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
24
26
  [![GitHub](https://img.shields.io/badge/GitHub-pytest--threadpool-blue?logo=github)](https://github.com/pytest-threadpool/pytest-threadpool)
25
27
  [![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
28
  [![codecov](https://codecov.io/gh/pytest-threadpool/pytest-threadpool/branch/main/graph/badge.svg)](https://codecov.io/gh/pytest-threadpool/pytest-threadpool)
27
29
 
28
- **Status: Beta** · Parallel test execution for free-threaded Python builds (3.13t+).
30
+ **Status: Beta** · Parallel test execution using threads.
29
31
 
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.
32
+ Runs test *bodies*, function-scoped fixture setup, and function-scoped fixture
33
+ teardown concurrently in a thread pool while keeping shared fixtures
34
+ (module/class/session scope) sequential.
35
+
36
+ Works on any Python 3.13+. Free-threaded builds (3.13t, 3.14t, 3.15t) get true
37
+ parallelism for CPU-bound tests. Standard builds still benefit from parallel
38
+ execution of I/O-bound tests (network, database, file operations).
33
39
 
34
40
  ## Installation
35
41
 
@@ -88,12 +94,12 @@ sequentially before workers launch and served from cache to all items.
88
94
 
89
95
  | Scope | Behavior |
90
96
  |------------|-------------------------------------------------|
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 |
97
+ | `function` | Cloned per-item, setup and teardown in parallel workers |
98
+ | `class` | Resolved once, cached, shared across items |
99
+ | `module` | Resolved once, cached, shared across items |
100
+ | `session` | Resolved once, cached, shared across items |
95
101
 
96
- Teardown for all scopes runs sequentially after the parallel group completes.
102
+ Shared fixture teardown runs sequentially after the parallel group completes.
97
103
 
98
104
  ## Marker levels
99
105
 
@@ -168,9 +174,12 @@ pytest
168
174
 
169
175
  | Component | Versions |
170
176
  |-----------|----------|
171
- | Python | 3.13t, 3.14t, 3.15t |
177
+ | Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
172
178
  | pytest | >=9.0.2 |
173
179
 
180
+ > **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
181
+ > for CPU-bound tests. I/O-bound tests still run concurrently.
182
+
174
183
  ## Known limitations
175
184
 
176
185
  - **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
@@ -1,17 +1,21 @@
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
+ [![Python](https://img.shields.io/badge/python-3.13%20%7C%203.13t%20%7C%203.14%20%7C%203.14t%20%7C%203.15%20%7C%203.15t-blue?logo=python&logoColor=white)](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)
8
8
  [![codecov](https://codecov.io/gh/pytest-threadpool/pytest-threadpool/branch/main/graph/badge.svg)](https://codecov.io/gh/pytest-threadpool/pytest-threadpool)
9
9
 
10
- **Status: Beta** · Parallel test execution for free-threaded Python builds (3.13t+).
10
+ **Status: Beta** · Parallel test execution using threads.
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
+
16
+ Works on any Python 3.13+. Free-threaded builds (3.13t, 3.14t, 3.15t) get true
17
+ parallelism for CPU-bound tests. Standard builds still benefit from parallel
18
+ execution of I/O-bound tests (network, database, file operations).
15
19
 
16
20
  ## Installation
17
21
 
@@ -70,12 +74,12 @@ sequentially before workers launch and served from cache to all items.
70
74
 
71
75
  | Scope | Behavior |
72
76
  |------------|-------------------------------------------------|
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 |
77
+ | `function` | Cloned per-item, setup and teardown in parallel workers |
78
+ | `class` | Resolved once, cached, shared across items |
79
+ | `module` | Resolved once, cached, shared across items |
80
+ | `session` | Resolved once, cached, shared across items |
77
81
 
78
- Teardown for all scopes runs sequentially after the parallel group completes.
82
+ Shared fixture teardown runs sequentially after the parallel group completes.
79
83
 
80
84
  ## Marker levels
81
85
 
@@ -150,9 +154,12 @@ pytest
150
154
 
151
155
  | Component | Versions |
152
156
  |-----------|----------|
153
- | Python | 3.13t, 3.14t, 3.15t |
157
+ | Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
154
158
  | pytest | >=9.0.2 |
155
159
 
160
+ > **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
161
+ > for CPU-bound tests. I/O-bound tests still run concurrently.
162
+
156
163
  ## Known limitations
157
164
 
158
165
  - **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
@@ -0,0 +1,11 @@
1
+ coverage:
2
+ status:
3
+ project:
4
+ default:
5
+ target: auto
6
+ threshold: 1%
7
+ patch:
8
+ default:
9
+ target: auto
10
+
11
+ comment: false
@@ -0,0 +1,3 @@
1
+ from pytest_threadpool import parallelizable
2
+
3
+ pytestmark = parallelizable("all")
@@ -0,0 +1,28 @@
1
+ """Fixtures resolving from the static DI container.
2
+
3
+ - ``request_handler`` injects a fresh RequestHandler per test (Factory).
4
+ - ``test_context`` injects a per-test TestContext (ContextLocal, reset per test).
5
+ """
6
+
7
+ import pytest
8
+
9
+ from examples.test_di.container import Container
10
+
11
+
12
+ @pytest.fixture
13
+ def request_handler():
14
+ """Fresh RequestHandler per test (Factory provider)."""
15
+ return Container.request_handler()
16
+
17
+
18
+ @pytest.fixture
19
+ def test_context():
20
+ """Per-test TestContext — ContextLocal + reset = test-local scope.
21
+
22
+ ContextLocal uses contextvars, so the instance follows the execution
23
+ context — not the OS thread. Safe across await boundaries.
24
+ The reset() in teardown gives the next test a fresh instance.
25
+ """
26
+ ctx = Container.test_context()
27
+ yield ctx
28
+ Container.test_context.reset()
@@ -0,0 +1,44 @@
1
+ """DI container wiring four provider scopes — pure stdlib, no GIL re-enablement.
2
+
3
+ - Singleton: one Config for the entire session
4
+ - ThreadLocal: one DbConnection per worker thread
5
+ - ContextLocal: one TestContext per test (survives await, reset between tests)
6
+ - Factory: fresh RequestHandler every injection
7
+
8
+ All providers are class attributes — use ``Container.config()`` etc. directly.
9
+
10
+ .. note::
11
+
12
+ The `dependency-injector <https://github.com/ets-labs/python-dependency-injector>`_
13
+ library offers similar provider scopes, but its C-extension is not marked as
14
+ free-threaded safe. On 3.13t+ builds Python will fall back to the GIL-enabled
15
+ interpreter when it is imported, negating the benefit of free-threaded execution.
16
+ This example uses pure-stdlib providers to avoid that limitation.
17
+ """
18
+
19
+ from examples.test_di.providers import ContextLocal, Factory, Singleton, ThreadLocal
20
+ from examples.test_di.services import Config, DbConnection, RequestHandler, TestContext
21
+
22
+
23
+ class Container:
24
+ config = Singleton(
25
+ Config,
26
+ db_url="postgresql://localhost/testdb",
27
+ pool_size=4,
28
+ )
29
+
30
+ db_connection = ThreadLocal(
31
+ DbConnection,
32
+ config=config,
33
+ )
34
+
35
+ test_context = ContextLocal(
36
+ TestContext,
37
+ config=config,
38
+ )
39
+
40
+ request_handler = Factory(
41
+ RequestHandler,
42
+ db=db_connection,
43
+ config=config,
44
+ )
@@ -0,0 +1,104 @@
1
+ """Pure-stdlib DI providers — no C extensions, no GIL re-enablement.
2
+
3
+ Four scopes built on threading and contextvars primitives:
4
+
5
+ - ``Singleton``: one instance for the process lifetime (thread-safe).
6
+ - ``ThreadLocal``: one instance per OS thread.
7
+ - ``ContextLocal``: one instance per execution context (survives await).
8
+ - ``Factory``: fresh instance on every call.
9
+ """
10
+
11
+ import contextlib
12
+ import contextvars
13
+ import threading
14
+ from typing import Any
15
+
16
+
17
+ class Singleton:
18
+ """Thread-safe singleton — one instance for the entire process."""
19
+
20
+ def __init__(self, cls: type, **kwargs: Any):
21
+ self._cls = cls
22
+ self._kwargs = kwargs
23
+ self._instance: Any = None
24
+ self._lock = threading.Lock()
25
+
26
+ def __call__(self) -> Any:
27
+ if self._instance is None:
28
+ with self._lock:
29
+ if self._instance is None:
30
+ self._instance = self._cls(**self._resolve_kwargs())
31
+ return self._instance
32
+
33
+ def reset(self) -> None:
34
+ with self._lock:
35
+ self._instance = None
36
+
37
+ def _resolve_kwargs(self) -> dict:
38
+ return {k: v() if callable(v) else v for k, v in self._kwargs.items()}
39
+
40
+
41
+ class ThreadLocal:
42
+ """Thread-local singleton — one instance per OS thread."""
43
+
44
+ def __init__(self, cls: type, **kwargs: Any):
45
+ self._cls = cls
46
+ self._kwargs = kwargs
47
+ self._local = threading.local()
48
+
49
+ def __call__(self) -> Any:
50
+ try:
51
+ return self._local.instance
52
+ except AttributeError:
53
+ self._local.instance = self._cls(**self._resolve_kwargs())
54
+ return self._local.instance
55
+
56
+ def reset(self) -> None:
57
+ with contextlib.suppress(AttributeError):
58
+ del self._local.instance
59
+
60
+ def _resolve_kwargs(self) -> dict:
61
+ return {k: v() if callable(v) else v for k, v in self._kwargs.items()}
62
+
63
+
64
+ class ContextLocal:
65
+ """Context-local singleton — one instance per execution context.
66
+
67
+ Uses ``contextvars.ContextVar``, so the instance follows the async
68
+ execution flow across ``await`` boundaries, even if the coroutine
69
+ resumes on a different OS thread.
70
+ """
71
+
72
+ _SENTINEL = object()
73
+
74
+ def __init__(self, cls: type, **kwargs: Any):
75
+ self._cls = cls
76
+ self._kwargs = kwargs
77
+ self._var: contextvars.ContextVar = contextvars.ContextVar(f"_di_{cls.__name__}")
78
+
79
+ def __call__(self) -> Any:
80
+ instance = self._var.get(self._SENTINEL)
81
+ if instance is self._SENTINEL:
82
+ instance = self._cls(**self._resolve_kwargs())
83
+ self._var.set(instance)
84
+ return instance
85
+
86
+ def reset(self) -> None:
87
+ self._var.set(self._SENTINEL) # type: ignore[arg-type]
88
+
89
+ def _resolve_kwargs(self) -> dict:
90
+ return {k: v() if callable(v) else v for k, v in self._kwargs.items()}
91
+
92
+
93
+ class Factory:
94
+ """Factory — fresh instance on every call."""
95
+
96
+ def __init__(self, cls: type, **kwargs: Any):
97
+ self._cls = cls
98
+ self._kwargs = kwargs
99
+
100
+ def __call__(self) -> Any:
101
+ return self._cls(**self._resolve_kwargs())
102
+
103
+ def _resolve_kwargs(self) -> dict:
104
+ return {k: v() if callable(v) else v for k, v in self._kwargs.items()}
@@ -0,0 +1,53 @@
1
+ """Minimal service classes for the DI example."""
2
+
3
+ import threading
4
+ import uuid
5
+
6
+
7
+ class Config:
8
+ """Application config — intended as a Singleton (shared across all tests)."""
9
+
10
+ def __init__(self, db_url: str, pool_size: int):
11
+ self.db_url = db_url
12
+ self.pool_size = pool_size
13
+ self.instance_id = uuid.uuid4()
14
+
15
+
16
+ class DbConnection:
17
+ """Database connection — intended as ThreadLocal (one per worker thread).
18
+
19
+ In a real app this would be sqlalchemy.Session, asyncpg.Connection, etc.
20
+ """
21
+
22
+ def __init__(self, config: Config):
23
+ self.config = config
24
+ self.instance_id = uuid.uuid4()
25
+ self.thread_id = threading.current_thread().ident
26
+
27
+
28
+ class TestContext:
29
+ """Per-test context — intended as ContextLocal (one per execution context).
30
+
31
+ In a real app this would be a request context, unit-of-work, or
32
+ transaction that should be the same instance within one test
33
+ but fresh for each test. Any function can resolve it from the
34
+ container without receiving it as a parameter.
35
+ """
36
+
37
+ def __init__(self, config: Config):
38
+ self.config = config
39
+ self.instance_id = uuid.uuid4()
40
+ self.data: dict = {}
41
+
42
+
43
+ class RequestHandler:
44
+ """Request handler — intended as Factory (fresh instance per injection).
45
+
46
+ In a real app this would be a use-case, service, or controller object
47
+ that gets a fresh instance per test / per request.
48
+ """
49
+
50
+ def __init__(self, db: DbConnection, config: Config):
51
+ self.db = db
52
+ self.config = config
53
+ self.instance_id = uuid.uuid4()
@@ -0,0 +1,19 @@
1
+ """Factory scope: fresh RequestHandler for every injection."""
2
+
3
+ import threading
4
+ from typing import ClassVar
5
+
6
+ import pytest
7
+
8
+
9
+ class TestFactory:
10
+ """RequestHandler is a Factory — every injection must be a fresh instance."""
11
+
12
+ _seen_ids: ClassVar[set] = set()
13
+ _lock = threading.Lock()
14
+
15
+ @pytest.mark.parametrize("_worker", range(6))
16
+ def test_handler_is_fresh(self, request_handler, _worker):
17
+ with self._lock:
18
+ assert request_handler.instance_id not in self._seen_ids, "Factory reused an instance"
19
+ self._seen_ids.add(request_handler.instance_id)