pytest-threadpool 0.3.3__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 (139) hide show
  1. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/.github/workflows/ci.yml +2 -1
  2. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/CONTRIBUTING.md +3 -1
  3. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/PKG-INFO +14 -4
  4. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/README.md +10 -3
  5. pytest_threadpool-0.3.4/codecov.yml +11 -0
  6. pytest_threadpool-0.3.4/examples/__init__.py +3 -0
  7. pytest_threadpool-0.3.4/examples/test_di/__init__.py +1 -0
  8. pytest_threadpool-0.3.4/examples/test_di/conftest.py +28 -0
  9. pytest_threadpool-0.3.4/examples/test_di/container.py +44 -0
  10. pytest_threadpool-0.3.4/examples/test_di/providers.py +104 -0
  11. pytest_threadpool-0.3.4/examples/test_di/services.py +53 -0
  12. pytest_threadpool-0.3.4/examples/test_di/test_factory.py +19 -0
  13. pytest_threadpool-0.3.4/examples/test_di/test_local.py +99 -0
  14. pytest_threadpool-0.3.4/examples/test_di/test_singleton.py +20 -0
  15. pytest_threadpool-0.3.4/examples/test_di/test_thread_local.py +77 -0
  16. pytest_threadpool-0.3.4/examples/test_logging/test_file_logging.py +30 -0
  17. pytest_threadpool-0.3.4/examples/test_logging/test_log_capture.py +66 -0
  18. pytest_threadpool-0.3.4/examples/test_logging/test_print_capture.py +44 -0
  19. pytest_threadpool-0.3.4/examples/test_queue/conftest.py +16 -0
  20. pytest_threadpool-0.3.4/examples/test_queue/test_queue.py +30 -0
  21. pytest_threadpool-0.3.4/examples/test_queue/user_pool.py +26 -0
  22. pytest_threadpool-0.3.4/examples/test_shared_state/__init__.py +0 -0
  23. pytest_threadpool-0.3.4/examples/test_shared_state/test_barrier.py +47 -0
  24. pytest_threadpool-0.3.4/examples/test_shared_state/test_counter.py +43 -0
  25. pytest_threadpool-0.3.4/examples/test_shared_state/test_counter_async.py +42 -0
  26. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/hooks/pre-commit +2 -2
  27. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/pyproject.toml +8 -1
  28. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_grouping.py +50 -2
  29. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_markers.py +23 -0
  30. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_runner.py +201 -30
  31. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_version.py +2 -2
  32. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/src/pytest_threadpool/plugin.py +5 -2
  33. pytest_threadpool-0.3.4/tests/__init__.py +0 -0
  34. pytest_threadpool-0.3.4/tests/integration_tests/cases/__init__.py +0 -0
  35. pytest_threadpool-0.3.4/tests/integration_tests/cases/capture_print_parallel.py +13 -0
  36. pytest_threadpool-0.3.4/tests/integration_tests/cases/capture_two_groups.py +30 -0
  37. pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_from_conftest.py +38 -0
  38. pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_from_conftest_conftest.py +9 -0
  39. pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_implicit_scope.py +56 -0
  40. pytest_threadpool-0.3.4/tests/integration_tests/cases/reporting_single_file_params.py +12 -0
  41. pytest_threadpool-0.3.4/tests/integration_tests/cases/reporting_stdout_during_parallel.py +13 -0
  42. pytest_threadpool-0.3.4/tests/integration_tests/test_capture.py +172 -0
  43. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_edge_cases.py +5 -7
  44. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_func_fixtures.py +23 -1
  45. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_package.py +78 -0
  46. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_reporting.py +127 -0
  47. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_grouping.py +65 -0
  48. pytest_threadpool-0.3.4/tests/unit_tests/test_unit_stream_proxy.py +122 -0
  49. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/.github/workflows/publish.yml +0 -0
  50. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/.gitignore +0 -0
  51. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/LICENSE +0 -0
  52. {pytest_threadpool-0.3.3/tests → pytest_threadpool-0.3.4/examples/test_logging}/__init__.py +0 -0
  53. {pytest_threadpool-0.3.3/tests/integration_tests/cases → pytest_threadpool-0.3.4/examples/test_queue}/__init__.py +0 -0
  54. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/scripts/setup-dev +0 -0
  55. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/src/pytest_threadpool/__init__.py +0 -0
  56. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_api.py +0 -0
  57. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_constants.py +0 -0
  58. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_fixtures.py +0 -0
  59. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/__init__.py +0 -0
  60. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/_templates.py +0 -0
  61. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
  62. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/class_single_method.py +0 -0
  63. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/class_thread_verification.py +0 -0
  64. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/collected_count.py +0 -0
  65. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
  66. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
  67. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
  68. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_sigint.py +0 -0
  69. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
  70. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_system_exit.py +0 -0
  71. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
  72. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
  73. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
  74. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_addfinalizer.py +0 -0
  75. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_chain.py +0 -0
  76. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_multiple_per_test.py +0 -0
  77. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_setup_parallel.py +0 -0
  78. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_with_shared_deps.py +0 -0
  79. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_with_tmp_path.py +0 -0
  80. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_with_xunit.py +0 -0
  81. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_yield_teardown.py +0 -0
  82. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
  83. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
  84. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
  85. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
  86. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
  87. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
  88. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/marks_custom.py +0 -0
  89. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/marks_standard.py +0 -0
  90. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
  91. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
  92. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_incremental.py +0 -0
  93. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
  94. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_package_children.py +0 -0
  95. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_all_merged.py +0 -0
  96. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
  97. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
  98. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
  99. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
  100. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_module_children.py +0 -0
  101. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
  102. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
  103. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_parameters.py +0 -0
  104. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
  105. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
  106. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/setup_all_fail.py +0 -0
  107. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
  108. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_counter.py +0 -0
  109. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
  110. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
  111. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
  112. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
  113. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/teardown_after_test_failure.py +0 -0
  114. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/teardown_parallel_timing.py +0 -0
  115. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/teardown_same_thread.py +0 -0
  116. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/teardown_xunit_function.py +0 -0
  117. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/teardown_xunit_method_thread.py +0 -0
  118. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/validate_threadpool.py +0 -0
  119. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
  120. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
  121. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
  122. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
  123. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
  124. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_method_teardown_runs.py +0 -0
  125. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
  126. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/conftest.py +0 -0
  127. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_class.py +0 -0
  128. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_fixtures.py +0 -0
  129. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_marks.py +0 -0
  130. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_parallel_teardown.py +0 -0
  131. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_scopes.py +0 -0
  132. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_sequential.py +0 -0
  133. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_shared.py +0 -0
  134. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_xunit.py +0 -0
  135. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/unit_tests/__init__.py +0 -0
  136. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_api.py +0 -0
  137. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_fixtures.py +0 -0
  138. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_markers.py +0 -0
  139. {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_plugin.py +0 -0
@@ -3,6 +3,7 @@ name: CI
3
3
  on:
4
4
  push:
5
5
  branches: [main]
6
+ tags-ignore: ["v*"]
6
7
  pull_request:
7
8
  branches: [main]
8
9
  workflow_call:
@@ -25,7 +26,7 @@ jobs:
25
26
  strategy:
26
27
  fail-fast: false
27
28
  matrix:
28
- python: ["3.13t", "3.14t", "3.15t"]
29
+ python: ["3.13", "3.13t", "3.14", "3.14t", "3.15", "3.15t"]
29
30
  steps:
30
31
  - uses: actions/checkout@v4
31
32
  with:
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-threadpool
3
- Version: 0.3.3
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,6 +10,9 @@ 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
@@ -18,18 +21,22 @@ Description-Content-Type: text/markdown
18
21
  # pytest-threadpool
19
22
 
20
23
  [![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/)
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/)
22
25
  [![License](https://img.shields.io/github/license/pytest-threadpool/pytest-threadpool)](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
23
26
  [![GitHub](https://img.shields.io/badge/GitHub-pytest--threadpool-blue?logo=github)](https://github.com/pytest-threadpool/pytest-threadpool)
24
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)
25
28
  [![codecov](https://codecov.io/gh/pytest-threadpool/pytest-threadpool/branch/main/graph/badge.svg)](https://codecov.io/gh/pytest-threadpool/pytest-threadpool)
26
29
 
27
- **Status: Beta** · Parallel test execution for free-threaded Python builds (3.13t+).
30
+ **Status: Beta** · Parallel test execution using threads.
28
31
 
29
32
  Runs test *bodies*, function-scoped fixture setup, and function-scoped fixture
30
33
  teardown concurrently in a thread pool while keeping shared fixtures
31
34
  (module/class/session scope) sequential.
32
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).
39
+
33
40
  ## Installation
34
41
 
35
42
  ```bash
@@ -167,9 +174,12 @@ pytest
167
174
 
168
175
  | Component | Versions |
169
176
  |-----------|----------|
170
- | Python | 3.13t, 3.14t, 3.15t |
177
+ | Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
171
178
  | pytest | >=9.0.2 |
172
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
+
173
183
  ## Known limitations
174
184
 
175
185
  - **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
@@ -1,18 +1,22 @@
1
1
  # pytest-threadpool
2
2
 
3
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/)
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
12
  Runs test *bodies*, function-scoped fixture setup, and function-scoped fixture
13
13
  teardown concurrently in a thread pool while keeping shared fixtures
14
14
  (module/class/session scope) sequential.
15
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).
19
+
16
20
  ## Installation
17
21
 
18
22
  ```bash
@@ -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)
@@ -0,0 +1,99 @@
1
+ """ContextLocal (per-test) scope: same instance within a test, fresh between tests.
2
+
3
+ Pattern: ContextLocal provider + per-test reset in fixture teardown.
4
+
5
+ Any function can resolve ``Container.test_context()`` and get the same
6
+ instance as the calling test — even across ``await`` boundaries that may
7
+ resume on a different thread. ``ContextLocal`` uses ``contextvars``
8
+ under the hood, so the context follows the execution flow, not the OS thread.
9
+
10
+ Parallel tests each run in their own context, so writes in one test
11
+ never leak into another.
12
+ """
13
+
14
+ import asyncio
15
+ import functools
16
+ import threading
17
+ from typing import ClassVar
18
+
19
+ import pytest
20
+
21
+ from examples.test_di.container import Container
22
+
23
+
24
+ def async_test(fn):
25
+ """Run an async test function in its own event loop."""
26
+
27
+ @functools.wraps(fn)
28
+ def wrapper(*args, **kwargs):
29
+ return asyncio.run(fn(*args, **kwargs))
30
+
31
+ return wrapper
32
+
33
+
34
+ def _helper_that_reads_from_container() -> dict:
35
+ """Simulate a function deep in the call stack resolving from the container."""
36
+ return Container.test_context().data
37
+
38
+
39
+ def _helper_that_writes_to_container(key: str, value: str) -> None:
40
+ """Simulate a function deep in the call stack writing to the context."""
41
+ Container.test_context().data[key] = value
42
+
43
+
44
+ async def _async_helper_that_reads_from_container() -> dict:
45
+ """Async function that resolves the context — may run on a different thread."""
46
+ await asyncio.sleep(0.01)
47
+ return Container.test_context().data
48
+
49
+
50
+ class TestLocal:
51
+ """Each test gets its own TestContext via ContextLocal + reset."""
52
+
53
+ _seen_ids: ClassVar[set] = set()
54
+ _lock = threading.Lock()
55
+
56
+ @pytest.mark.parametrize("_worker", range(6))
57
+ def test_write_then_read_from_container(self, test_context, _worker):
58
+ """Test writes to context, a helper resolves the same context from the container."""
59
+ test_context.data["worker"] = str(_worker)
60
+
61
+ data = _helper_that_reads_from_container()
62
+
63
+ assert data["worker"] == str(_worker)
64
+ assert data is test_context.data
65
+
66
+ @pytest.mark.parametrize("_worker", range(6))
67
+ def test_helper_writes_visible_to_test(self, test_context, _worker):
68
+ """A helper writes via the container, the test sees it through its fixture."""
69
+ _helper_that_writes_to_container("source", "helper")
70
+
71
+ assert test_context.data["source"] == "helper"
72
+
73
+ @pytest.mark.parametrize("_worker", range(6))
74
+ def test_parallel_tests_are_isolated(self, test_context, _worker):
75
+ """Each parallel test starts with an empty context — no cross-test leakage."""
76
+ assert test_context.data == {}, f"Context leaked from another test: {test_context.data}"
77
+ test_context.data["mine"] = str(_worker)
78
+
79
+ @pytest.mark.parametrize("_worker", range(6))
80
+ def test_context_is_fresh_per_test(self, test_context, _worker):
81
+ """Each test invocation gets a unique TestContext instance."""
82
+ with self._lock:
83
+ assert test_context.instance_id not in self._seen_ids, (
84
+ "ContextLocal+reset reused an instance across tests"
85
+ )
86
+ self._seen_ids.add(test_context.instance_id)
87
+
88
+ @async_test
89
+ @pytest.mark.parametrize("_worker", range(6))
90
+ async def test_context_survives_await(self, test_context, _worker):
91
+ """Context is preserved across await boundaries that may switch threads."""
92
+ test_context.data["before_await"] = str(_worker)
93
+
94
+ await asyncio.sleep(0.01)
95
+
96
+ data = await _async_helper_that_reads_from_container()
97
+
98
+ assert data["before_await"] == str(_worker)
99
+ assert data is test_context.data
@@ -0,0 +1,20 @@
1
+ """Singleton scope: one Config instance shared across all tests."""
2
+
3
+ import threading
4
+ from typing import ClassVar
5
+
6
+ import pytest
7
+
8
+
9
+ class TestSingleton:
10
+ """Config is a Singleton — every test must see the exact same instance."""
11
+
12
+ _seen_ids: ClassVar[set] = set()
13
+ _lock = threading.Lock()
14
+
15
+ @pytest.mark.parametrize("_worker", range(6))
16
+ def test_config_is_shared(self, request_handler, _worker):
17
+ config = request_handler.config
18
+ with self._lock:
19
+ self._seen_ids.add(config.instance_id)
20
+ assert len(self._seen_ids) == 1, f"Singleton produced {len(self._seen_ids)} instances"
@@ -0,0 +1,77 @@
1
+ """ThreadLocal scope: one DbConnection per worker thread.
2
+
3
+ ThreadLocal binds to the OS thread. This means it does NOT survive
4
+ ``await`` boundaries — if the event loop resumes a coroutine on a
5
+ different thread, a new instance is created. Use ContextLocal
6
+ (see test_local.py) when you need context that follows the execution
7
+ flow across awaits.
8
+ """
9
+
10
+ import asyncio
11
+ import functools
12
+ import threading
13
+ from typing import ClassVar
14
+
15
+ import pytest
16
+
17
+ from examples.test_di.container import Container
18
+
19
+
20
+ def async_test(fn):
21
+ """Run an async test function in its own event loop."""
22
+
23
+ @functools.wraps(fn)
24
+ def wrapper(*args, **kwargs):
25
+ return asyncio.run(fn(*args, **kwargs))
26
+
27
+ return wrapper
28
+
29
+
30
+ class TestThreadLocal:
31
+ """DbConnection is ThreadLocal — same instance within a thread,
32
+ different instances across threads.
33
+ """
34
+
35
+ _ids_by_thread: ClassVar[dict] = {}
36
+ _lock = threading.Lock()
37
+
38
+ @pytest.mark.parametrize("_worker", range(6))
39
+ def test_db_is_per_thread(self, request_handler, _worker):
40
+ db = request_handler.db
41
+ tid = threading.current_thread().ident
42
+ with self._lock:
43
+ if tid in self._ids_by_thread:
44
+ assert self._ids_by_thread[tid] == db.instance_id, (
45
+ "ThreadLocal returned different instance on same thread"
46
+ )
47
+ else:
48
+ self._ids_by_thread[tid] = db.instance_id
49
+
50
+ @async_test
51
+ @pytest.mark.parametrize("_worker", range(6))
52
+ async def test_thread_local_does_not_survive_await(self, _worker):
53
+ """ThreadLocal may return a different instance after an await.
54
+
55
+ If ``asyncio.run()`` uses a thread pool internally, an ``await``
56
+ can resume on a different OS thread — and ThreadLocal is keyed
57
+ by thread, so it produces a new instance. This is the key
58
+ difference from ContextLocal.
59
+ """
60
+ before = Container.db_connection()
61
+ before_tid = threading.current_thread().ident
62
+
63
+ results: dict = {}
64
+
65
+ async def resolve_after_await():
66
+ await asyncio.sleep(0.01)
67
+ results["db"] = Container.db_connection()
68
+ results["tid"] = threading.current_thread().ident
69
+
70
+ await resolve_after_await()
71
+
72
+ if before_tid != results["tid"]:
73
+ assert before.instance_id != results["db"].instance_id, (
74
+ "ThreadLocal returned same instance on different thread"
75
+ )
76
+ else:
77
+ assert before.instance_id == results["db"].instance_id
@@ -0,0 +1,30 @@
1
+ """Per-test file logging — each test writes to its own log file.
2
+
3
+ When you need persistent debug output that survives the test run
4
+ (e.g. for CI artifacts), write to a file in ``tmp_path``. Each test
5
+ gets its own temporary directory, so no coordination is needed.
6
+
7
+ After the run, inspect logs in the ``tmp_path_factory`` base dir
8
+ or configure CI to upload the ``/tmp/pytest-*`` tree as artifacts.
9
+ """
10
+
11
+ from time import sleep
12
+
13
+ import pytest
14
+
15
+
16
+ class TestFileLogging:
17
+ """Each test writes debug output to its own temp file."""
18
+
19
+ @pytest.mark.parametrize("_worker", range(4))
20
+ def test_per_test_log_file(self, _worker, tmp_path):
21
+ """Write debug data to a file in tmp_path — isolated per test."""
22
+ log_file = tmp_path / "debug.log"
23
+
24
+ with log_file.open("w") as f:
25
+ f.write(f"worker {_worker}: starting\n")
26
+ sleep(0.01)
27
+ f.write(f"worker {_worker}: done\n")
28
+
29
+ lines = log_file.read_text().splitlines()
30
+ assert lines == [f"worker {_worker}: starting", f"worker {_worker}: done"]