pytest-threadpool 0.3.4__tar.gz → 0.3.5__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 (164) hide show
  1. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/.github/workflows/ci.yml +16 -2
  2. pytest_threadpool-0.3.5/.pre-commit-config.yaml +17 -0
  3. pytest_threadpool-0.3.5/CHANGELOG.md +19 -0
  4. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/CONTRIBUTING.md +0 -3
  5. pytest_threadpool-0.3.4/README.md → pytest_threadpool-0.3.5/PKG-INFO +87 -36
  6. pytest_threadpool-0.3.4/PKG-INFO → pytest_threadpool-0.3.5/README.md +64 -56
  7. pytest_threadpool-0.3.5/ROADMAP.md +83 -0
  8. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/codecov.yml +3 -0
  9. pytest_threadpool-0.3.5/examples/test_event_bus/event_bus.py +85 -0
  10. pytest_threadpool-0.3.5/examples/test_event_bus/test_event_bus.py +112 -0
  11. pytest_threadpool-0.3.5/examples/test_logging/test_log_capture.py +34 -0
  12. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_queue/user_pool.py +2 -2
  13. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/pyproject.toml +38 -5
  14. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/scripts/setup-dev +1 -1
  15. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_grouping.py +9 -0
  16. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_markers.py +1 -4
  17. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_runner.py +11 -0
  18. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_version.py +2 -2
  19. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/src/pytest_threadpool/plugin.py +4 -1
  20. pytest_threadpool-0.3.5/tests/__init__.py +0 -0
  21. pytest_threadpool-0.3.5/tests/integration_tests/cases/__init__.py +0 -0
  22. pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_first_item_setup_fail.py +32 -0
  23. pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_maxfail_parallel.py +24 -0
  24. pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_teardown_exception.py +32 -0
  25. pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_worker_exception.py +23 -0
  26. pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_worker_exception_conftest.py +14 -0
  27. pytest_threadpool-0.3.5/tests/integration_tests/cases/fixture_func_mixed_parallel.py +45 -0
  28. pytest_threadpool-0.3.5/tests/integration_tests/cases/fixture_func_setup_parallel.py +48 -0
  29. pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_collect_only.py +16 -0
  30. pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_setup_only.py +20 -0
  31. pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_setup_show.py +17 -0
  32. pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_single_parallel_item.py +23 -0
  33. pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_single_worker.py +25 -0
  34. pytest_threadpool-0.3.5/tests/integration_tests/cases/scope_children_on_function.py +15 -0
  35. pytest_threadpool-0.3.5/tests/integration_tests/cases/scope_mixed_fail_skip.py +38 -0
  36. pytest_threadpool-0.3.5/tests/integration_tests/cases/scope_two_classes_mixed.py +48 -0
  37. pytest_threadpool-0.3.5/tests/integration_tests/cases/xunit_function_mixed_parallel.py +60 -0
  38. pytest_threadpool-0.3.5/tests/integration_tests/cases/xunit_method_mixed_parallel.py +49 -0
  39. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/conftest.py +22 -1
  40. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_edge_cases.py +145 -2
  41. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_func_fixtures.py +6 -0
  42. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_scopes.py +12 -0
  43. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_xunit.py +12 -0
  44. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_grouping.py +55 -0
  45. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_markers.py +70 -0
  46. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_plugin.py +21 -0
  47. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_stream_proxy.py +4 -4
  48. pytest_threadpool-0.3.4/examples/test_logging/test_file_logging.py +0 -30
  49. pytest_threadpool-0.3.4/examples/test_logging/test_log_capture.py +0 -66
  50. pytest_threadpool-0.3.4/examples/test_logging/test_print_capture.py +0 -44
  51. pytest_threadpool-0.3.4/hooks/pre-commit +0 -13
  52. pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_setup_parallel.py +0 -51
  53. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/.github/workflows/publish.yml +0 -0
  54. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/.gitignore +0 -0
  55. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/LICENSE +0 -0
  56. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/__init__.py +0 -0
  57. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_di/__init__.py +0 -0
  58. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_di/conftest.py +0 -0
  59. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_di/container.py +0 -0
  60. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_di/providers.py +0 -0
  61. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_di/services.py +0 -0
  62. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_di/test_factory.py +0 -0
  63. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_di/test_local.py +0 -0
  64. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_di/test_singleton.py +0 -0
  65. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_di/test_thread_local.py +0 -0
  66. {pytest_threadpool-0.3.4/examples/test_logging → pytest_threadpool-0.3.5/examples/test_event_bus}/__init__.py +0 -0
  67. {pytest_threadpool-0.3.4/examples/test_queue → pytest_threadpool-0.3.5/examples/test_logging}/__init__.py +0 -0
  68. {pytest_threadpool-0.3.4/examples/test_shared_state → pytest_threadpool-0.3.5/examples/test_queue}/__init__.py +0 -0
  69. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_queue/conftest.py +0 -0
  70. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_queue/test_queue.py +0 -0
  71. {pytest_threadpool-0.3.4/tests → pytest_threadpool-0.3.5/examples/test_shared_state}/__init__.py +0 -0
  72. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_shared_state/test_barrier.py +0 -0
  73. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_shared_state/test_counter.py +0 -0
  74. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/examples/test_shared_state/test_counter_async.py +0 -0
  75. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/src/pytest_threadpool/__init__.py +0 -0
  76. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_api.py +0 -0
  77. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_constants.py +0 -0
  78. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_fixtures.py +0 -0
  79. /pytest_threadpool-0.3.4/tests/integration_tests/cases/__init__.py → /pytest_threadpool-0.3.5/src/pytest_threadpool/py.typed +0 -0
  80. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/__init__.py +0 -0
  81. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/_templates.py +0 -0
  82. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/capture_print_parallel.py +0 -0
  83. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/capture_two_groups.py +0 -0
  84. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
  85. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/class_single_method.py +0 -0
  86. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/class_thread_verification.py +0 -0
  87. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/collected_count.py +0 -0
  88. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
  89. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
  90. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
  91. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_sigint.py +0 -0
  92. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
  93. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_system_exit.py +0 -0
  94. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
  95. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
  96. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
  97. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_addfinalizer.py +0 -0
  98. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_chain.py +0 -0
  99. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_from_conftest.py +0 -0
  100. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_from_conftest_conftest.py +0 -0
  101. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_implicit_scope.py +0 -0
  102. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_multiple_per_test.py +0 -0
  103. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_with_shared_deps.py +0 -0
  104. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_with_tmp_path.py +0 -0
  105. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_with_xunit.py +0 -0
  106. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_yield_teardown.py +0 -0
  107. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
  108. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
  109. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
  110. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
  111. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
  112. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
  113. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/marks_custom.py +0 -0
  114. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/marks_standard.py +0 -0
  115. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
  116. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
  117. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_incremental.py +0 -0
  118. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
  119. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_package_children.py +0 -0
  120. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_single_file_params.py +0 -0
  121. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_stdout_during_parallel.py +0 -0
  122. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_all_merged.py +0 -0
  123. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
  124. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
  125. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
  126. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
  127. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_module_children.py +0 -0
  128. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
  129. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
  130. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_parameters.py +0 -0
  131. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
  132. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
  133. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/setup_all_fail.py +0 -0
  134. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
  135. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_counter.py +0 -0
  136. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
  137. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
  138. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
  139. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
  140. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_after_test_failure.py +0 -0
  141. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_parallel_timing.py +0 -0
  142. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_same_thread.py +0 -0
  143. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_xunit_function.py +0 -0
  144. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_xunit_method_thread.py +0 -0
  145. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/validate_threadpool.py +0 -0
  146. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
  147. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
  148. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
  149. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
  150. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
  151. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_method_teardown_runs.py +0 -0
  152. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
  153. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_capture.py +0 -0
  154. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_class.py +0 -0
  155. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_fixtures.py +0 -0
  156. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_marks.py +0 -0
  157. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_package.py +0 -0
  158. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_parallel_teardown.py +0 -0
  159. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_reporting.py +0 -0
  160. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_sequential.py +0 -0
  161. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_shared.py +0 -0
  162. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/unit_tests/__init__.py +0 -0
  163. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_api.py +0 -0
  164. {pytest_threadpool-0.3.4 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_fixtures.py +0 -0
@@ -34,9 +34,23 @@ jobs:
34
34
  - uses: astral-sh/setup-uv@v6
35
35
  - run: uv python install ${{ matrix.python }}
36
36
  - run: uv sync --python ${{ matrix.python }} --group dev
37
- - run: uv run --python ${{ matrix.python }} pytest -v --tb=short --cov --cov-branch --cov-report=xml
37
+ - name: Run tests
38
+ id: tests
39
+ env:
40
+ COVERAGE_PROCESS_START: pyproject.toml
41
+ run: uv run --python ${{ matrix.python }} coverage run -m pytest -v --tb=short
42
+ - name: Generate coverage report
43
+ if: always() && steps.tests.outcome != 'cancelled'
44
+ run: |
45
+ uv run --python ${{ matrix.python }} coverage combine
46
+ uv run --python ${{ matrix.python }} coverage report --format=markdown >> "$GITHUB_STEP_SUMMARY"
47
+ uv run --python ${{ matrix.python }} coverage xml
48
+ sed -i 's|<source></source>|<source>.</source>|' coverage.xml
38
49
  - name: Upload coverage reports to Codecov
39
- if: matrix.python == '3.14t'
50
+ if: always() && matrix.python == '3.14t' && steps.tests.outcome != 'cancelled'
40
51
  uses: codecov/codecov-action@v5
41
52
  with:
42
53
  token: ${{ secrets.CODECOV_TOKEN }}
54
+ files: coverage.xml
55
+ fail_ci_if_error: true
56
+ verbose: true
@@ -0,0 +1,17 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.6
4
+ hooks:
5
+ - id: ruff-format
6
+ args: [--check, src/, tests/]
7
+ - id: ruff
8
+ args: [src/, tests/]
9
+
10
+ - repo: local
11
+ hooks:
12
+ - id: pyright
13
+ name: pyright
14
+ entry: uv run pyright src/
15
+ language: system
16
+ types: [python]
17
+ pass_filenames: false
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.3.5
4
+
5
+ ### Fixes
6
+
7
+ - Fixed crash (KeyError) when a worker thread throws an unexpected exception
8
+ outside the normal test lifecycle — failures are now reported gracefully
9
+ instead of taking down the entire test run.
10
+ - `--threadpool` with a non-numeric value (e.g. `--threadpool foo`) now raises
11
+ a clean `pytest.UsageError` instead of an unhandled `ValueError` traceback.
12
+ - `@parallelizable("children")` applied directly to a test function now emits
13
+ a warning explaining that functions have no children, instead of being
14
+ silently ignored.
15
+
16
+ ### Cleanup
17
+
18
+ - Moved `ParallelScope` import to module top in `_markers.py`, removed
19
+ misleading circular-import comment.
@@ -127,9 +127,6 @@ src/pytest_threadpool/
127
127
  _runner.py # ParallelRunner: parallel execution orchestration
128
128
  plugin.py # pytest hook implementations (wiring only)
129
129
 
130
- hooks/
131
- pre-commit # Git pre-commit hook (ruff + pyright)
132
-
133
130
  scripts/
134
131
  setup-dev # One-command dev environment setup
135
132
  ```
@@ -1,3 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-threadpool
3
+ Version: 0.3.5
4
+ Summary: Parallel test execution for free-threaded Python builds
5
+ Project-URL: Homepage, https://github.com/pytest-threadpool/pytest-threadpool
6
+ Project-URL: Source, https://github.com/pytest-threadpool/pytest-threadpool
7
+ Project-URL: Issues, https://github.com/pytest-threadpool/pytest-threadpool/issues
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: free-threaded,nogil,parallel,pytest,pytest parallel,testing,threadpool
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: pytest<=9.0.2,>=9.0.0
22
+ Description-Content-Type: text/markdown
23
+
1
24
  # pytest-threadpool
2
25
 
3
26
  [![PyPI - Version](https://img.shields.io/pypi/v/pytest-threadpool)](https://pypi.org/project/pytest-threadpool/)
@@ -36,22 +59,28 @@ from pytest_threadpool import parallelizable, not_parallelizable
36
59
 
37
60
  import pytest
38
61
 
39
- @parallelizable("children") # all nested tests run in parallel
62
+
63
+ @parallelizable("children") # all nested tests run in parallel
40
64
  class TestMyFeature:
41
- def test_a(self): ...
42
- def test_b(self): ...
65
+ def test_a(self): ...
66
+
67
+ def test_b(self): ...
43
68
 
44
- @parallelizable("parameters") # parametrized variants run in parallel
69
+
70
+ @parallelizable("parameters") # parametrized variants run in parallel
45
71
  @pytest.mark.parametrize("x", [1, 2, 3])
46
72
  def test_with_params(x): ...
47
73
 
48
- @parallelizable("all") # children + parameters combined
74
+
75
+ @parallelizable("all") # children + parameters combined
49
76
  class TestEverything:
50
- @pytest.mark.parametrize("n", [1, 2])
51
- def test_param(self, n): ...
52
- def test_plain(self): ...
77
+ @pytest.mark.parametrize("n", [1, 2])
78
+ def test_param(self, n): ...
79
+
80
+ def test_plain(self): ...
53
81
 
54
- @not_parallelizable # opt out of inherited parallelism
82
+
83
+ @not_parallelizable # opt out of inherited parallelism
55
84
  def test_must_be_sequential(): ...
56
85
  ```
57
86
 
@@ -72,8 +101,8 @@ state between concurrent fixture setups.
72
101
  Shared fixtures (module, class, and session scope) are resolved once
73
102
  sequentially before workers launch and served from cache to all items.
74
103
 
75
- | Scope | Behavior |
76
- |------------|-------------------------------------------------|
104
+ | Scope | Behavior |
105
+ |------------|---------------------------------------------------------|
77
106
  | `function` | Cloned per-item, setup and teardown in parallel workers |
78
107
  | `class` | Resolved once, cached, shared across items |
79
108
  | `module` | Resolved once, cached, shared across items |
@@ -103,33 +132,33 @@ import pytest
103
132
 
104
133
 
105
134
  class SharedState:
106
- lock = threading.Lock() # not pickleable
107
- event = threading.Event() # not pickleable
108
- results = {}
135
+ lock = threading.Lock() # not pickleable
136
+ event = threading.Event() # not pickleable
137
+ results = {}
109
138
 
110
139
 
111
140
  @pytest.mark.parallelizable("children")
112
141
  class TestGroupA:
113
- def test_a1(self):
114
- with SharedState.lock:
115
- SharedState.results["a1"] = True
142
+ def test_a1(self):
143
+ with SharedState.lock:
144
+ SharedState.results["a1"] = True
116
145
 
117
- def test_a2(self):
118
- with SharedState.lock:
119
- SharedState.results["a2"] = True
146
+ def test_a2(self):
147
+ with SharedState.lock:
148
+ SharedState.results["a2"] = True
120
149
 
121
150
 
122
151
  @pytest.mark.parallelizable("children")
123
152
  class TestGroupB:
124
- def test_b1(self):
125
- SharedState.event.set()
126
- with SharedState.lock:
127
- SharedState.results["b1"] = True
128
-
129
- def test_b2(self):
130
- assert SharedState.event.wait(timeout=10)
131
- with SharedState.lock:
132
- SharedState.results["b2"] = True
153
+ def test_b1(self):
154
+ SharedState.event.set()
155
+ with SharedState.lock:
156
+ SharedState.results["b1"] = True
157
+
158
+ def test_b2(self):
159
+ assert SharedState.event.wait(timeout=10)
160
+ with SharedState.lock:
161
+ SharedState.results["b2"] = True
133
162
  ```
134
163
 
135
164
  Objects like `threading.Lock`, `threading.Event`, `logging.Logger`, database
@@ -152,14 +181,28 @@ pytest
152
181
 
153
182
  ## Tested versions
154
183
 
155
- | Component | Versions |
156
- |-----------|----------|
184
+ | Component | Versions |
185
+ |-----------|---------------------------------------|
157
186
  | Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
158
- | pytest | >=9.0.2 |
187
+ | pytest | 9.0.2 |
159
188
 
160
189
  > **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
161
190
  > for CPU-bound tests. I/O-bound tests still run concurrently.
162
191
 
192
+ ## Examples
193
+
194
+ The [`examples/`](examples/) directory contains runnable usage patterns:
195
+
196
+ - **DI container** — dependency injection with Singleton, ThreadLocal, ContextLocal, and Factory scopes
197
+ - **Event bus** — shared in-memory test double with concurrent producers and aggregate verification
198
+ - **Parallel logging** — shared thread-safe log collector (caplog alternative)
199
+ - **Shared state** — barriers, atomic counters, and cross-group coordination
200
+ - **User pool** — custom thread pool with LIFO queue recycling
201
+
202
+ The [`tests/integration_tests/cases/`](tests/integration_tests/cases/) and
203
+ [`tests/integration_tests/`](tests/integration_tests/) directories are also
204
+ worth browsing for real-world grouping, fixture, and reporting scenarios.
205
+
163
206
  ## Known limitations
164
207
 
165
208
  - **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
@@ -168,10 +211,18 @@ pytest
168
211
  - **No plugin compatibility guarantees** — Interactions with other pytest
169
212
  plugins (e.g. `pytest-xdist`, `pytest-timeout`, `pytest-randomly`) are
170
213
  untested and may conflict.
171
- - **No captured stdout in parallel** — Standard output from tests running
172
- concurrently is written directly to the terminal. Pytest's built-in capture
173
- (`capsys`/`capfd`) is not supported during parallel execution.
214
+ - **No `capsys`/`capfd`/`caplog` in parallel** — These fixtures are not
215
+ thread-safe. `capsys`/`capfd` fail with "cannot use capsys and capsys at
216
+ the same time" when requested by parallel tests. `caplog` leaks records
217
+ across fixtures and its `at_level()` context managers race on the shared
218
+ root logger. Alternatives:
219
+ - **`print()`** — Worker output is suppressed by default (buffered by
220
+ thread-local stream proxies). Pass `-s` (`--capture=no`) to disable
221
+ suppression and see interleaved output.
222
+ - **Logging** — Use a per-test `FileHandler` writing to `tmp_path`, or
223
+ collect structured records in a shared thread-safe list (see
224
+ [`examples/test_logging/`](examples/test_logging/)).
174
225
 
175
226
  ## License
176
227
 
177
- MIT
228
+ [Apache 2.0](LICENSE)
@@ -1,23 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: pytest-threadpool
3
- Version: 0.3.4
4
- Summary: Parallel test execution for free-threaded Python builds
5
- License-Expression: MIT
6
- License-File: LICENSE
7
- Keywords: free-threaded,nogil,parallel,pytest,pytest parallel,testing,threadpool
8
- Classifier: Development Status :: 4 - Beta
9
- Classifier: Framework :: Pytest
10
- Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
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
16
- Classifier: Topic :: Software Development :: Testing
17
- Requires-Python: >=3.13
18
- Requires-Dist: pytest>=9.0.2
19
- Description-Content-Type: text/markdown
20
-
21
1
  # pytest-threadpool
22
2
 
23
3
  [![PyPI - Version](https://img.shields.io/pypi/v/pytest-threadpool)](https://pypi.org/project/pytest-threadpool/)
@@ -56,22 +36,28 @@ from pytest_threadpool import parallelizable, not_parallelizable
56
36
 
57
37
  import pytest
58
38
 
59
- @parallelizable("children") # all nested tests run in parallel
39
+
40
+ @parallelizable("children") # all nested tests run in parallel
60
41
  class TestMyFeature:
61
- def test_a(self): ...
62
- def test_b(self): ...
42
+ def test_a(self): ...
43
+
44
+ def test_b(self): ...
63
45
 
64
- @parallelizable("parameters") # parametrized variants run in parallel
46
+
47
+ @parallelizable("parameters") # parametrized variants run in parallel
65
48
  @pytest.mark.parametrize("x", [1, 2, 3])
66
49
  def test_with_params(x): ...
67
50
 
68
- @parallelizable("all") # children + parameters combined
51
+
52
+ @parallelizable("all") # children + parameters combined
69
53
  class TestEverything:
70
- @pytest.mark.parametrize("n", [1, 2])
71
- def test_param(self, n): ...
72
- def test_plain(self): ...
54
+ @pytest.mark.parametrize("n", [1, 2])
55
+ def test_param(self, n): ...
56
+
57
+ def test_plain(self): ...
73
58
 
74
- @not_parallelizable # opt out of inherited parallelism
59
+
60
+ @not_parallelizable # opt out of inherited parallelism
75
61
  def test_must_be_sequential(): ...
76
62
  ```
77
63
 
@@ -92,8 +78,8 @@ state between concurrent fixture setups.
92
78
  Shared fixtures (module, class, and session scope) are resolved once
93
79
  sequentially before workers launch and served from cache to all items.
94
80
 
95
- | Scope | Behavior |
96
- |------------|-------------------------------------------------|
81
+ | Scope | Behavior |
82
+ |------------|---------------------------------------------------------|
97
83
  | `function` | Cloned per-item, setup and teardown in parallel workers |
98
84
  | `class` | Resolved once, cached, shared across items |
99
85
  | `module` | Resolved once, cached, shared across items |
@@ -123,33 +109,33 @@ import pytest
123
109
 
124
110
 
125
111
  class SharedState:
126
- lock = threading.Lock() # not pickleable
127
- event = threading.Event() # not pickleable
128
- results = {}
112
+ lock = threading.Lock() # not pickleable
113
+ event = threading.Event() # not pickleable
114
+ results = {}
129
115
 
130
116
 
131
117
  @pytest.mark.parallelizable("children")
132
118
  class TestGroupA:
133
- def test_a1(self):
134
- with SharedState.lock:
135
- SharedState.results["a1"] = True
119
+ def test_a1(self):
120
+ with SharedState.lock:
121
+ SharedState.results["a1"] = True
136
122
 
137
- def test_a2(self):
138
- with SharedState.lock:
139
- SharedState.results["a2"] = True
123
+ def test_a2(self):
124
+ with SharedState.lock:
125
+ SharedState.results["a2"] = True
140
126
 
141
127
 
142
128
  @pytest.mark.parallelizable("children")
143
129
  class TestGroupB:
144
- def test_b1(self):
145
- SharedState.event.set()
146
- with SharedState.lock:
147
- SharedState.results["b1"] = True
148
-
149
- def test_b2(self):
150
- assert SharedState.event.wait(timeout=10)
151
- with SharedState.lock:
152
- SharedState.results["b2"] = True
130
+ def test_b1(self):
131
+ SharedState.event.set()
132
+ with SharedState.lock:
133
+ SharedState.results["b1"] = True
134
+
135
+ def test_b2(self):
136
+ assert SharedState.event.wait(timeout=10)
137
+ with SharedState.lock:
138
+ SharedState.results["b2"] = True
153
139
  ```
154
140
 
155
141
  Objects like `threading.Lock`, `threading.Event`, `logging.Logger`, database
@@ -172,14 +158,28 @@ pytest
172
158
 
173
159
  ## Tested versions
174
160
 
175
- | Component | Versions |
176
- |-----------|----------|
161
+ | Component | Versions |
162
+ |-----------|---------------------------------------|
177
163
  | Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
178
- | pytest | >=9.0.2 |
164
+ | pytest | 9.0.2 |
179
165
 
180
166
  > **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
181
167
  > for CPU-bound tests. I/O-bound tests still run concurrently.
182
168
 
169
+ ## Examples
170
+
171
+ The [`examples/`](examples/) directory contains runnable usage patterns:
172
+
173
+ - **DI container** — dependency injection with Singleton, ThreadLocal, ContextLocal, and Factory scopes
174
+ - **Event bus** — shared in-memory test double with concurrent producers and aggregate verification
175
+ - **Parallel logging** — shared thread-safe log collector (caplog alternative)
176
+ - **Shared state** — barriers, atomic counters, and cross-group coordination
177
+ - **User pool** — custom thread pool with LIFO queue recycling
178
+
179
+ The [`tests/integration_tests/cases/`](tests/integration_tests/cases/) and
180
+ [`tests/integration_tests/`](tests/integration_tests/) directories are also
181
+ worth browsing for real-world grouping, fixture, and reporting scenarios.
182
+
183
183
  ## Known limitations
184
184
 
185
185
  - **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
@@ -188,10 +188,18 @@ pytest
188
188
  - **No plugin compatibility guarantees** — Interactions with other pytest
189
189
  plugins (e.g. `pytest-xdist`, `pytest-timeout`, `pytest-randomly`) are
190
190
  untested and may conflict.
191
- - **No captured stdout in parallel** — Standard output from tests running
192
- concurrently is written directly to the terminal. Pytest's built-in capture
193
- (`capsys`/`capfd`) is not supported during parallel execution.
191
+ - **No `capsys`/`capfd`/`caplog` in parallel** — These fixtures are not
192
+ thread-safe. `capsys`/`capfd` fail with "cannot use capsys and capsys at
193
+ the same time" when requested by parallel tests. `caplog` leaks records
194
+ across fixtures and its `at_level()` context managers race on the shared
195
+ root logger. Alternatives:
196
+ - **`print()`** — Worker output is suppressed by default (buffered by
197
+ thread-local stream proxies). Pass `-s` (`--capture=no`) to disable
198
+ suppression and see interleaved output.
199
+ - **Logging** — Use a per-test `FileHandler` writing to `tmp_path`, or
200
+ collect structured records in a shared thread-safe list (see
201
+ [`examples/test_logging/`](examples/test_logging/)).
194
202
 
195
203
  ## License
196
204
 
197
- MIT
205
+ [Apache 2.0](LICENSE)
@@ -0,0 +1,83 @@
1
+ # Roadmap
2
+
3
+ ## Capture fixture support (capsys / capfd / caplog)
4
+
5
+ **Goal:** Make `capsys`, `capfd`, and `caplog` work transparently in parallel tests.
6
+
7
+ **Problem:** These fixtures assume exclusive ownership of global state (`sys.stdout`,
8
+ root logger) during a test's lifetime. When multiple tests run concurrently, output
9
+ interleaves and pytest raises "cannot use capsys and capsys at the same time".
10
+ `caplog` leaks records across tests, and `at_level()` context managers race on the
11
+ shared root logger.
12
+
13
+ **Approach:** Delayed replay — collect output per-worker during parallel execution,
14
+ then feed it back one item at a time during the sequential reporting phase.
15
+
16
+ - **capsys/capfd:** The `_ThreadLocalStream` proxy already buffers worker output
17
+ into per-thread `StringIO`. Instead of discarding on `deactivate()`, associate
18
+ each buffer with its test item and flush to the real stream before
19
+ `pytest_runtest_makereport` reads the capture.
20
+
21
+ - **caplog:** Install a per-thread `logging.Handler` that collects records into a
22
+ thread-local list. During sequential reporting, replay records into caplog's
23
+ handler so the fixture sees the correct records for each test.
24
+
25
+ **Edge cases to handle:**
26
+ - Nested captures (fixtures that call `capsys.readouterr()` mid-test)
27
+ - `caplog.at_level()` context managers across threads
28
+ - Fixtures that mix capsys with print output
29
+ - `capfd` (file descriptor level) vs `capsys` (sys.stdout level)
30
+
31
+ **Priority:** High — this is the most common friction point for adoption.
32
+
33
+ ## Harden FixtureDef cloning
34
+
35
+ **Goal:** Make function-scoped fixture cloning more robust against pytest internals
36
+ changes and more transparent to developers.
37
+
38
+ **Current approach:** `clone_function_fixturedefs` uses `__new__` + `__dict__.update`
39
+ for a shallow copy, then resets `cached_result` and `_finalizers`. This works because
40
+ those are the only two fields mutated during fixture lifecycle — but it's fragile.
41
+
42
+ **Improvements:**
43
+
44
+ - **Deep-copy guard:** Validate at clone time that the only mutable fields are the
45
+ ones we reset. If a future pytest version adds mutable state to `FixtureDef`,
46
+ the clone should fail loudly (assertion or warning) rather than silently sharing
47
+ state across workers.
48
+
49
+ - **Document fixture chain isolation:** When fixture A (function-scoped) depends on
50
+ fixture B (also function-scoped), both get cloned independently. Chain resolution
51
+ works because `_arg2fixturedefs` is replaced as a whole, so `getfixturevalue("B")`
52
+ hits B's clone. This is correct but non-obvious — add inline comments explaining
53
+ the chain resolution path through the cloned map.
54
+
55
+ - **`object.__setattr__` on `_arg2fixturedefs`:** Currently bypasses the `Final`
56
+ typing annotation. If pytest ever enforces immutability at runtime (frozen
57
+ dataclass, `__slots__`), this breaks. Track whether upstream adds a setter or
58
+ public mutation API.
59
+
60
+ **Priority:** Medium — current implementation works, but one pytest release could
61
+ break it silently.
62
+
63
+ ## Plugin compatibility testing
64
+
65
+ Validate and document interactions with commonly used pytest plugins:
66
+
67
+ - `pytest-randomly` — test reordering vs parallel group formation
68
+ - `pytest-timeout` — per-test timeouts in worker threads
69
+ - `pytest-repeat` — repeated test execution in parallel groups
70
+ - `pytest-cov` — coverage collection across worker threads
71
+
72
+ ## Public pytest API migration
73
+
74
+ Track pytest releases for public equivalents of internal APIs currently used:
75
+
76
+ - `_pytest.fixtures.FixtureDef._finalizers`
77
+ - `_pytest.fixtures.FixtureDef.cached_result`
78
+ - `_pytest.setupplan.SetupState.stack`
79
+ - `_pytest.scope.Scope`
80
+ - `callspec._arg2scope`
81
+
82
+ Migrate to public APIs as they become available to reduce breakage risk
83
+ across pytest releases.
@@ -8,4 +8,7 @@ coverage:
8
8
  default:
9
9
  target: auto
10
10
 
11
+ ignore:
12
+ - "src/pytest_threadpool/_version.py"
13
+
11
14
  comment: false
@@ -0,0 +1,85 @@
1
+ """In-memory event bus — thread-safe test double.
2
+
3
+ A minimal pub/sub bus that records all published events. Tests publish
4
+ events concurrently, then verify delivery, ordering, and deduplication
5
+ against the shared log.
6
+
7
+ With pytest-xdist this would require an external broker (Redis, RabbitMQ)
8
+ or a socket-based mock server since each worker is a separate process.
9
+ With pytest-threadpool it's a plain Python object protected by a Lock.
10
+ """
11
+
12
+ import threading
13
+ from typing import ClassVar
14
+
15
+
16
+ class EventBus:
17
+ """Thread-safe in-memory event bus for testing."""
18
+
19
+ _lock = threading.Lock()
20
+ _events: ClassVar[list[dict]] = []
21
+ _subscribers: ClassVar[dict[str, list]] = {}
22
+ _waiters: ClassVar[list[tuple[str | None, int, threading.Event]]] = []
23
+
24
+ @classmethod
25
+ def publish(cls, topic: str, payload: dict) -> None:
26
+ with cls._lock:
27
+ event = {"topic": topic, "payload": payload, "thread": threading.current_thread().name}
28
+ cls._events.append(event)
29
+ for callback in cls._subscribers.get(topic, []):
30
+ callback(event)
31
+ cls._check_waiters()
32
+
33
+ @classmethod
34
+ def subscribe(cls, topic: str, callback) -> None:
35
+ with cls._lock:
36
+ cls._subscribers.setdefault(topic, []).append(callback)
37
+
38
+ @classmethod
39
+ def events(cls, topic: str | None = None) -> list[dict]:
40
+ with cls._lock:
41
+ if topic is None:
42
+ return list(cls._events)
43
+ return [e for e in cls._events if e["topic"] == topic]
44
+
45
+ @classmethod
46
+ def wait_for(cls, count: int, topic: str | None = None, timeout: float = 10) -> list[dict]:
47
+ """Block until at least ``count`` events match, then return them."""
48
+ ready = threading.Event()
49
+ with cls._lock:
50
+ matched = cls._filter(topic)
51
+ if len(matched) >= count:
52
+ return matched
53
+ cls._waiters.append((topic, count, ready))
54
+ if not ready.wait(timeout=timeout):
55
+ actual = len(cls.events(topic))
56
+ label = f"topic={topic!r}" if topic else "all topics"
57
+ raise TimeoutError(
58
+ f"EventBus.wait_for: expected {count} events on {label}, "
59
+ f"got {actual} after {timeout}s"
60
+ )
61
+ return cls.events(topic)
62
+
63
+ @classmethod
64
+ def reset(cls) -> None:
65
+ with cls._lock:
66
+ cls._events.clear()
67
+ cls._subscribers.clear()
68
+ cls._waiters.clear()
69
+
70
+ @classmethod
71
+ def _check_waiters(cls) -> None:
72
+ """Signal any waiters whose condition is met. Must hold _lock."""
73
+ remaining = []
74
+ for topic, count, event in cls._waiters:
75
+ if len(cls._filter(topic)) >= count:
76
+ event.set()
77
+ else:
78
+ remaining.append((topic, count, event))
79
+ cls._waiters[:] = remaining
80
+
81
+ @classmethod
82
+ def _filter(cls, topic: str | None) -> list[dict]:
83
+ if topic is None:
84
+ return list(cls._events)
85
+ return [e for e in cls._events if e["topic"] == topic]