pytest-threadpool 0.3.3__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.
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/.github/workflows/ci.yml +18 -3
- pytest_threadpool-0.3.5/.pre-commit-config.yaml +17 -0
- pytest_threadpool-0.3.5/CHANGELOG.md +19 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/CONTRIBUTING.md +3 -4
- pytest_threadpool-0.3.5/PKG-INFO +228 -0
- pytest_threadpool-0.3.3/PKG-INFO → pytest_threadpool-0.3.5/README.md +74 -56
- pytest_threadpool-0.3.5/ROADMAP.md +83 -0
- pytest_threadpool-0.3.5/codecov.yml +14 -0
- pytest_threadpool-0.3.5/examples/__init__.py +3 -0
- pytest_threadpool-0.3.5/examples/test_di/__init__.py +1 -0
- pytest_threadpool-0.3.5/examples/test_di/conftest.py +28 -0
- pytest_threadpool-0.3.5/examples/test_di/container.py +44 -0
- pytest_threadpool-0.3.5/examples/test_di/providers.py +104 -0
- pytest_threadpool-0.3.5/examples/test_di/services.py +53 -0
- pytest_threadpool-0.3.5/examples/test_di/test_factory.py +19 -0
- pytest_threadpool-0.3.5/examples/test_di/test_local.py +99 -0
- pytest_threadpool-0.3.5/examples/test_di/test_singleton.py +20 -0
- pytest_threadpool-0.3.5/examples/test_di/test_thread_local.py +77 -0
- pytest_threadpool-0.3.5/examples/test_event_bus/event_bus.py +85 -0
- pytest_threadpool-0.3.5/examples/test_event_bus/test_event_bus.py +112 -0
- pytest_threadpool-0.3.5/examples/test_logging/test_log_capture.py +34 -0
- pytest_threadpool-0.3.5/examples/test_queue/__init__.py +0 -0
- pytest_threadpool-0.3.5/examples/test_queue/conftest.py +16 -0
- pytest_threadpool-0.3.5/examples/test_queue/test_queue.py +30 -0
- pytest_threadpool-0.3.5/examples/test_queue/user_pool.py +26 -0
- pytest_threadpool-0.3.5/examples/test_shared_state/__init__.py +0 -0
- pytest_threadpool-0.3.5/examples/test_shared_state/test_barrier.py +47 -0
- pytest_threadpool-0.3.5/examples/test_shared_state/test_counter.py +43 -0
- pytest_threadpool-0.3.5/examples/test_shared_state/test_counter_async.py +42 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/pyproject.toml +45 -5
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/scripts/setup-dev +1 -1
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_grouping.py +59 -2
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_markers.py +24 -4
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_runner.py +212 -30
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_version.py +2 -2
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/src/pytest_threadpool/plugin.py +9 -3
- pytest_threadpool-0.3.5/src/pytest_threadpool/py.typed +0 -0
- pytest_threadpool-0.3.5/tests/__init__.py +0 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/__init__.py +0 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/capture_print_parallel.py +13 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/capture_two_groups.py +30 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_first_item_setup_fail.py +32 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_maxfail_parallel.py +24 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_teardown_exception.py +32 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_worker_exception.py +23 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/edge_worker_exception_conftest.py +14 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/fixture_func_from_conftest.py +38 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/fixture_func_from_conftest_conftest.py +9 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/fixture_func_implicit_scope.py +56 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/fixture_func_mixed_parallel.py +45 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/fixture_func_setup_parallel.py +48 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/reporting_single_file_params.py +12 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/reporting_stdout_during_parallel.py +13 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_collect_only.py +16 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_setup_only.py +20 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_setup_show.py +17 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_single_parallel_item.py +23 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/runner_single_worker.py +25 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/scope_children_on_function.py +15 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/scope_mixed_fail_skip.py +38 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/scope_two_classes_mixed.py +48 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/xunit_function_mixed_parallel.py +60 -0
- pytest_threadpool-0.3.5/tests/integration_tests/cases/xunit_method_mixed_parallel.py +49 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/conftest.py +22 -1
- pytest_threadpool-0.3.5/tests/integration_tests/test_capture.py +172 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_edge_cases.py +149 -8
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_func_fixtures.py +29 -1
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_package.py +78 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_reporting.py +127 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_scopes.py +12 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_xunit.py +12 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_grouping.py +120 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_markers.py +70 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_plugin.py +21 -0
- pytest_threadpool-0.3.5/tests/unit_tests/test_unit_stream_proxy.py +122 -0
- pytest_threadpool-0.3.3/README.md +0 -170
- pytest_threadpool-0.3.3/hooks/pre-commit +0 -13
- pytest_threadpool-0.3.3/tests/integration_tests/cases/fixture_func_setup_parallel.py +0 -51
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/.github/workflows/publish.yml +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/.gitignore +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/LICENSE +0 -0
- {pytest_threadpool-0.3.3/tests → pytest_threadpool-0.3.5/examples/test_event_bus}/__init__.py +0 -0
- {pytest_threadpool-0.3.3/tests/integration_tests/cases → pytest_threadpool-0.3.5/examples/test_logging}/__init__.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/src/pytest_threadpool/__init__.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_api.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_constants.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/src/pytest_threadpool/_fixtures.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/__init__.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/_templates.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/class_single_method.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/class_thread_verification.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/collected_count.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_sigint.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/edge_system_exit.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_addfinalizer.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_chain.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_multiple_per_test.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_with_shared_deps.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_with_tmp_path.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_with_xunit.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_func_yield_teardown.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/marks_custom.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/marks_standard.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_incremental.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/reporting_package_children.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_all_merged.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_module_children.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/scope_parameters.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/setup_all_fail.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_counter.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_after_test_failure.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_parallel_timing.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_same_thread.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_xunit_function.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/teardown_xunit_method_thread.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/validate_threadpool.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_method_teardown_runs.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_class.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_fixtures.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_marks.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_parallel_teardown.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_sequential.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/integration_tests/test_pytester_shared.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/unit_tests/__init__.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_api.py +0 -0
- {pytest_threadpool-0.3.3 → pytest_threadpool-0.3.5}/tests/unit_tests/test_unit_fixtures.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:
|
|
@@ -33,9 +34,23 @@ jobs:
|
|
|
33
34
|
- uses: astral-sh/setup-uv@v6
|
|
34
35
|
- run: uv python install ${{ matrix.python }}
|
|
35
36
|
- run: uv sync --python ${{ matrix.python }} --group dev
|
|
36
|
-
-
|
|
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
|
|
37
49
|
- name: Upload coverage reports to Codecov
|
|
38
|
-
if: matrix.python == '3.14t'
|
|
50
|
+
if: always() && matrix.python == '3.14t' && steps.tests.outcome != 'cancelled'
|
|
39
51
|
uses: codecov/codecov-action@v5
|
|
40
52
|
with:
|
|
41
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.
|
|
@@ -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
|
-
>
|
|
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
|
|
|
@@ -125,9 +127,6 @@ src/pytest_threadpool/
|
|
|
125
127
|
_runner.py # ParallelRunner: parallel execution orchestration
|
|
126
128
|
plugin.py # pytest hook implementations (wiring only)
|
|
127
129
|
|
|
128
|
-
hooks/
|
|
129
|
-
pre-commit # Git pre-commit hook (ruff + pyright)
|
|
130
|
-
|
|
131
130
|
scripts/
|
|
132
131
|
setup-dev # One-command dev environment setup
|
|
133
132
|
```
|
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
|
|
24
|
+
# pytest-threadpool
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/pytest-threadpool/)
|
|
27
|
+
[](https://pypi.org/project/pytest-threadpool/)
|
|
28
|
+
[](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
|
|
29
|
+
[](https://github.com/pytest-threadpool/pytest-threadpool)
|
|
30
|
+
[](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml)
|
|
31
|
+
[](https://codecov.io/gh/pytest-threadpool/pytest-threadpool)
|
|
32
|
+
|
|
33
|
+
**Status: Beta** · Parallel test execution using threads.
|
|
34
|
+
|
|
35
|
+
Runs test *bodies*, function-scoped fixture setup, and function-scoped fixture
|
|
36
|
+
teardown concurrently in a thread pool while keeping shared fixtures
|
|
37
|
+
(module/class/session scope) sequential.
|
|
38
|
+
|
|
39
|
+
Works on any Python 3.13+. Free-threaded builds (3.13t, 3.14t, 3.15t) get true
|
|
40
|
+
parallelism for CPU-bound tests. Standard builds still benefit from parallel
|
|
41
|
+
execution of I/O-bound tests (network, database, file operations).
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install pytest-threadpool
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pytest --threadpool auto
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Mark tests for parallel execution:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from pytest_threadpool import parallelizable, not_parallelizable
|
|
59
|
+
|
|
60
|
+
import pytest
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@parallelizable("children") # all nested tests run in parallel
|
|
64
|
+
class TestMyFeature:
|
|
65
|
+
def test_a(self): ...
|
|
66
|
+
|
|
67
|
+
def test_b(self): ...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@parallelizable("parameters") # parametrized variants run in parallel
|
|
71
|
+
@pytest.mark.parametrize("x", [1, 2, 3])
|
|
72
|
+
def test_with_params(x): ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@parallelizable("all") # children + parameters combined
|
|
76
|
+
class TestEverything:
|
|
77
|
+
@pytest.mark.parametrize("n", [1, 2])
|
|
78
|
+
def test_param(self, n): ...
|
|
79
|
+
|
|
80
|
+
def test_plain(self): ...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@not_parallelizable # opt out of inherited parallelism
|
|
84
|
+
def test_must_be_sequential(): ...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Scopes
|
|
88
|
+
|
|
89
|
+
| Scope | Effect |
|
|
90
|
+
|--------------|-------------------------------------------------------------------|
|
|
91
|
+
| `children` | All nested tests run concurrently (children, grandchildren, etc.) |
|
|
92
|
+
| `parameters` | Parametrized variants of the same test run concurrently |
|
|
93
|
+
| `all` | Combines `children` + `parameters` |
|
|
94
|
+
|
|
95
|
+
## Fixture handling
|
|
96
|
+
|
|
97
|
+
Function-scoped fixtures are cloned per-item and set up in parallel alongside
|
|
98
|
+
test calls. Each worker gets independent fixture instances — no shared mutable
|
|
99
|
+
state between concurrent fixture setups.
|
|
100
|
+
|
|
101
|
+
Shared fixtures (module, class, and session scope) are resolved once
|
|
102
|
+
sequentially before workers launch and served from cache to all items.
|
|
103
|
+
|
|
104
|
+
| Scope | Behavior |
|
|
105
|
+
|------------|---------------------------------------------------------|
|
|
106
|
+
| `function` | Cloned per-item, setup and teardown in parallel workers |
|
|
107
|
+
| `class` | Resolved once, cached, shared across items |
|
|
108
|
+
| `module` | Resolved once, cached, shared across items |
|
|
109
|
+
| `session` | Resolved once, cached, shared across items |
|
|
110
|
+
|
|
111
|
+
Shared fixture teardown runs sequentially after the parallel group completes.
|
|
112
|
+
|
|
113
|
+
## Marker levels
|
|
114
|
+
|
|
115
|
+
Markers can be applied at function, class, module (`pytestmark`), or
|
|
116
|
+
package (`__init__.py` `pytestmark`) level. Priority (most specific wins):
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
not_parallelizable > own marker > class > module > package
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Shared state between tests
|
|
123
|
+
|
|
124
|
+
Unlike `pytest-xdist`, which uses subprocesses and requires all test data to
|
|
125
|
+
be pickleable, `pytest-threadpool` runs tests in threads within a **single
|
|
126
|
+
process**. This means tests can share common non-pickleable, thread-safe
|
|
127
|
+
objects — both within a parallel group and across sequential groups:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import threading
|
|
131
|
+
import pytest
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class SharedState:
|
|
135
|
+
lock = threading.Lock() # not pickleable
|
|
136
|
+
event = threading.Event() # not pickleable
|
|
137
|
+
results = {}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.mark.parallelizable("children")
|
|
141
|
+
class TestGroupA:
|
|
142
|
+
def test_a1(self):
|
|
143
|
+
with SharedState.lock:
|
|
144
|
+
SharedState.results["a1"] = True
|
|
145
|
+
|
|
146
|
+
def test_a2(self):
|
|
147
|
+
with SharedState.lock:
|
|
148
|
+
SharedState.results["a2"] = True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.mark.parallelizable("children")
|
|
152
|
+
class TestGroupB:
|
|
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
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Objects like `threading.Lock`, `threading.Event`, `logging.Logger`, database
|
|
165
|
+
connections, and other non-pickleable resources can live as class attributes
|
|
166
|
+
and be accessed freely from any test — parallel or sequential — without
|
|
167
|
+
serialization overhead or workarounds.
|
|
168
|
+
|
|
169
|
+
## Usage
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Auto-detect thread count
|
|
173
|
+
pytest --threadpool auto
|
|
174
|
+
|
|
175
|
+
# Fixed thread count
|
|
176
|
+
pytest --threadpool 8
|
|
177
|
+
|
|
178
|
+
# Normal sequential run (no flag)
|
|
179
|
+
pytest
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Tested versions
|
|
183
|
+
|
|
184
|
+
| Component | Versions |
|
|
185
|
+
|-----------|---------------------------------------|
|
|
186
|
+
| Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
|
|
187
|
+
| pytest | 9.0.2 |
|
|
188
|
+
|
|
189
|
+
> **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
|
|
190
|
+
> for CPU-bound tests. I/O-bound tests still run concurrently.
|
|
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
|
+
|
|
206
|
+
## Known limitations
|
|
207
|
+
|
|
208
|
+
- **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
|
|
209
|
+
(fixture finalizers, setup state, terminal writer) that have no public
|
|
210
|
+
equivalents. These may break across pytest releases without warning.
|
|
211
|
+
- **No plugin compatibility guarantees** — Interactions with other pytest
|
|
212
|
+
plugins (e.g. `pytest-xdist`, `pytest-timeout`, `pytest-randomly`) are
|
|
213
|
+
untested and may conflict.
|
|
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/)).
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
[Apache 2.0](LICENSE)
|
|
@@ -1,35 +1,22 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pytest-threadpool
|
|
3
|
-
Version: 0.3.3
|
|
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: Topic :: Software Development :: Testing
|
|
14
|
-
Requires-Python: >=3.13
|
|
15
|
-
Requires-Dist: pytest>=9.0.2
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
|
|
18
1
|
# pytest-threadpool
|
|
19
2
|
|
|
20
3
|
[](https://pypi.org/project/pytest-threadpool/)
|
|
21
|
-
[](https://pypi.org/project/pytest-threadpool/)
|
|
22
5
|
[](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
|
|
23
6
|
[](https://github.com/pytest-threadpool/pytest-threadpool)
|
|
24
7
|
[](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml)
|
|
25
8
|
[](https://codecov.io/gh/pytest-threadpool/pytest-threadpool)
|
|
26
9
|
|
|
27
|
-
**Status: Beta** · Parallel test execution
|
|
10
|
+
**Status: Beta** · Parallel test execution using threads.
|
|
28
11
|
|
|
29
12
|
Runs test *bodies*, function-scoped fixture setup, and function-scoped fixture
|
|
30
13
|
teardown concurrently in a thread pool while keeping shared fixtures
|
|
31
14
|
(module/class/session scope) sequential.
|
|
32
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
|
+
|
|
33
20
|
## Installation
|
|
34
21
|
|
|
35
22
|
```bash
|
|
@@ -49,22 +36,28 @@ from pytest_threadpool import parallelizable, not_parallelizable
|
|
|
49
36
|
|
|
50
37
|
import pytest
|
|
51
38
|
|
|
52
|
-
|
|
39
|
+
|
|
40
|
+
@parallelizable("children") # all nested tests run in parallel
|
|
53
41
|
class TestMyFeature:
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
def test_a(self): ...
|
|
43
|
+
|
|
44
|
+
def test_b(self): ...
|
|
56
45
|
|
|
57
|
-
|
|
46
|
+
|
|
47
|
+
@parallelizable("parameters") # parametrized variants run in parallel
|
|
58
48
|
@pytest.mark.parametrize("x", [1, 2, 3])
|
|
59
49
|
def test_with_params(x): ...
|
|
60
50
|
|
|
61
|
-
|
|
51
|
+
|
|
52
|
+
@parallelizable("all") # children + parameters combined
|
|
62
53
|
class TestEverything:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
@pytest.mark.parametrize("n", [1, 2])
|
|
55
|
+
def test_param(self, n): ...
|
|
56
|
+
|
|
57
|
+
def test_plain(self): ...
|
|
66
58
|
|
|
67
|
-
|
|
59
|
+
|
|
60
|
+
@not_parallelizable # opt out of inherited parallelism
|
|
68
61
|
def test_must_be_sequential(): ...
|
|
69
62
|
```
|
|
70
63
|
|
|
@@ -85,8 +78,8 @@ state between concurrent fixture setups.
|
|
|
85
78
|
Shared fixtures (module, class, and session scope) are resolved once
|
|
86
79
|
sequentially before workers launch and served from cache to all items.
|
|
87
80
|
|
|
88
|
-
| Scope | Behavior
|
|
89
|
-
|
|
81
|
+
| Scope | Behavior |
|
|
82
|
+
|------------|---------------------------------------------------------|
|
|
90
83
|
| `function` | Cloned per-item, setup and teardown in parallel workers |
|
|
91
84
|
| `class` | Resolved once, cached, shared across items |
|
|
92
85
|
| `module` | Resolved once, cached, shared across items |
|
|
@@ -116,33 +109,33 @@ import pytest
|
|
|
116
109
|
|
|
117
110
|
|
|
118
111
|
class SharedState:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
112
|
+
lock = threading.Lock() # not pickleable
|
|
113
|
+
event = threading.Event() # not pickleable
|
|
114
|
+
results = {}
|
|
122
115
|
|
|
123
116
|
|
|
124
117
|
@pytest.mark.parallelizable("children")
|
|
125
118
|
class TestGroupA:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
def test_a1(self):
|
|
120
|
+
with SharedState.lock:
|
|
121
|
+
SharedState.results["a1"] = True
|
|
129
122
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
def test_a2(self):
|
|
124
|
+
with SharedState.lock:
|
|
125
|
+
SharedState.results["a2"] = True
|
|
133
126
|
|
|
134
127
|
|
|
135
128
|
@pytest.mark.parallelizable("children")
|
|
136
129
|
class TestGroupB:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
139
|
```
|
|
147
140
|
|
|
148
141
|
Objects like `threading.Lock`, `threading.Event`, `logging.Logger`, database
|
|
@@ -165,10 +158,27 @@ pytest
|
|
|
165
158
|
|
|
166
159
|
## Tested versions
|
|
167
160
|
|
|
168
|
-
| Component | Versions
|
|
169
|
-
|
|
170
|
-
| Python | 3.13t, 3.14t, 3.15t |
|
|
171
|
-
| pytest |
|
|
161
|
+
| Component | Versions |
|
|
162
|
+
|-----------|---------------------------------------|
|
|
163
|
+
| Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
|
|
164
|
+
| pytest | 9.0.2 |
|
|
165
|
+
|
|
166
|
+
> **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
|
|
167
|
+
> for CPU-bound tests. I/O-bound tests still run concurrently.
|
|
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.
|
|
172
182
|
|
|
173
183
|
## Known limitations
|
|
174
184
|
|
|
@@ -178,10 +188,18 @@ pytest
|
|
|
178
188
|
- **No plugin compatibility guarantees** — Interactions with other pytest
|
|
179
189
|
plugins (e.g. `pytest-xdist`, `pytest-timeout`, `pytest-randomly`) are
|
|
180
190
|
untested and may conflict.
|
|
181
|
-
- **No
|
|
182
|
-
|
|
183
|
-
|
|
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/)).
|
|
184
202
|
|
|
185
203
|
## License
|
|
186
204
|
|
|
187
|
-
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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()
|