pytest-threadpool 0.3.0__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/.github/workflows/ci.yml +7 -1
- pytest_threadpool-0.3.4/.github/workflows/publish.yml +25 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/.gitignore +3 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/CONTRIBUTING.md +38 -21
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/PKG-INFO +23 -14
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/README.md +19 -12
- pytest_threadpool-0.3.4/codecov.yml +11 -0
- pytest_threadpool-0.3.4/examples/__init__.py +3 -0
- pytest_threadpool-0.3.4/examples/test_di/__init__.py +1 -0
- pytest_threadpool-0.3.4/examples/test_di/conftest.py +28 -0
- pytest_threadpool-0.3.4/examples/test_di/container.py +44 -0
- pytest_threadpool-0.3.4/examples/test_di/providers.py +104 -0
- pytest_threadpool-0.3.4/examples/test_di/services.py +53 -0
- pytest_threadpool-0.3.4/examples/test_di/test_factory.py +19 -0
- pytest_threadpool-0.3.4/examples/test_di/test_local.py +99 -0
- pytest_threadpool-0.3.4/examples/test_di/test_singleton.py +20 -0
- pytest_threadpool-0.3.4/examples/test_di/test_thread_local.py +77 -0
- pytest_threadpool-0.3.4/examples/test_logging/test_file_logging.py +30 -0
- pytest_threadpool-0.3.4/examples/test_logging/test_log_capture.py +66 -0
- pytest_threadpool-0.3.4/examples/test_logging/test_print_capture.py +44 -0
- pytest_threadpool-0.3.4/examples/test_queue/conftest.py +16 -0
- pytest_threadpool-0.3.4/examples/test_queue/test_queue.py +30 -0
- pytest_threadpool-0.3.4/examples/test_queue/user_pool.py +26 -0
- pytest_threadpool-0.3.4/examples/test_shared_state/test_barrier.py +47 -0
- pytest_threadpool-0.3.4/examples/test_shared_state/test_counter.py +43 -0
- pytest_threadpool-0.3.4/examples/test_shared_state/test_counter_async.py +42 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/hooks/pre-commit +2 -2
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/pyproject.toml +18 -4
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_grouping.py +50 -2
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_markers.py +23 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_runner.py +270 -59
- pytest_threadpool-0.3.4/src/pytest_threadpool/_version.py +34 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/plugin.py +5 -2
- pytest_threadpool-0.3.4/tests/__init__.py +0 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/__init__.py +0 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/capture_print_parallel.py +13 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/capture_two_groups.py +30 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/collected_count.py +23 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_from_conftest.py +38 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_from_conftest_conftest.py +9 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/fixture_func_implicit_scope.py +56 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/reporting_single_file_params.py +12 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/reporting_stdout_during_parallel.py +13 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_after_test_failure.py +38 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_parallel_timing.py +55 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_same_thread.py +50 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_xunit_function.py +51 -0
- pytest_threadpool-0.3.4/tests/integration_tests/cases/teardown_xunit_method_thread.py +49 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/conftest.py +9 -0
- pytest_threadpool-0.3.4/tests/integration_tests/test_capture.py +172 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_edge_cases.py +5 -7
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_func_fixtures.py +23 -1
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_package.py +78 -0
- pytest_threadpool-0.3.4/tests/integration_tests/test_pytester_parallel_teardown.py +42 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_reporting.py +136 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_grouping.py +65 -0
- pytest_threadpool-0.3.4/tests/unit_tests/test_unit_stream_proxy.py +122 -0
- pytest_threadpool-0.3.0/.claude/settings.local.json +0 -30
- pytest_threadpool-0.3.0/ANALYSIS.md +0 -142
- pytest_threadpool-0.3.0/PLAN.md +0 -223
- pytest_threadpool-0.3.0/TODO.md +0 -32
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/LICENSE +0 -0
- {pytest_threadpool-0.3.0/tests → pytest_threadpool-0.3.4/examples/test_logging}/__init__.py +0 -0
- {pytest_threadpool-0.3.0/tests/integration_tests/cases → pytest_threadpool-0.3.4/examples/test_queue}/__init__.py +0 -0
- /pytest_threadpool-0.3.0/CLAUDE.md → /pytest_threadpool-0.3.4/examples/test_shared_state/__init__.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/scripts/setup-dev +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/__init__.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_api.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_constants.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/src/pytest_threadpool/_fixtures.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/__init__.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/_templates.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/class_barrier_concurrency.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/class_single_method.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/class_thread_verification.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_cross_module_group.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_keyboard_interrupt.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_nested_threads.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_sigint.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_sigint_many.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/edge_system_exit.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_autouse_function.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_class_scoped_once.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_class_yield.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_addfinalizer.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_chain.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_multiple_per_test.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_setup_parallel.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_with_shared_deps.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_with_tmp_path.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_with_xunit.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_func_yield_teardown.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_function_scoped.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_interdependent_finalizers.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_multiple_scopes.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_parameterized.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_teardown_exception.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/fixture_yield_cleanup.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/marks_custom.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/marks_standard.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/parallel_only_skip.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_cross_module.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_incremental.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_incremental_conftest.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/reporting_package_children.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_all_merged.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_all_not_parallelizable.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_children_separate_params.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_dynamic_parametrize.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_method_overrides_class.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_module_children.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_not_parallelizable_function.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_not_parallelizable_method.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/scope_parameters.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/sequential_bare_functions.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/sequential_unmarked_class.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/setup_all_fail.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/setup_mixed_pass_fail.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_counter.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_cross_group_non_pickleable.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_dict_mutation.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_non_pickleable.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/shared_two_phase_barrier.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/validate_threadpool.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/validate_threadpool_conftest.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_class_setup.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_combined_setup.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_function_setup.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_method_setup.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_method_teardown_runs.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/cases/xunit_module_setup.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_class.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_fixtures.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_marks.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_scopes.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_sequential.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_shared.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/integration_tests/test_pytester_xunit.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/__init__.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_api.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_fixtures.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_markers.py +0 -0
- {pytest_threadpool-0.3.0 → pytest_threadpool-0.3.4}/tests/unit_tests/test_unit_plugin.py +0 -0
|
@@ -3,14 +3,18 @@ name: CI
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
branches: [main]
|
|
6
|
+
tags-ignore: ["v*"]
|
|
6
7
|
pull_request:
|
|
7
8
|
branches: [main]
|
|
9
|
+
workflow_call:
|
|
8
10
|
|
|
9
11
|
jobs:
|
|
10
12
|
lint:
|
|
11
13
|
runs-on: ubuntu-latest
|
|
12
14
|
steps:
|
|
13
15
|
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
14
18
|
- uses: astral-sh/setup-uv@v6
|
|
15
19
|
- run: uv sync --python 3.14t --group dev
|
|
16
20
|
- run: uv run ruff format --check src/ tests/
|
|
@@ -22,9 +26,11 @@ jobs:
|
|
|
22
26
|
strategy:
|
|
23
27
|
fail-fast: false
|
|
24
28
|
matrix:
|
|
25
|
-
python: ["3.13t", "3.14t", "3.15t"]
|
|
29
|
+
python: ["3.13", "3.13t", "3.14", "3.14t", "3.15", "3.15t"]
|
|
26
30
|
steps:
|
|
27
31
|
- uses: actions/checkout@v4
|
|
32
|
+
with:
|
|
33
|
+
fetch-depth: 0
|
|
28
34
|
- uses: astral-sh/setup-uv@v6
|
|
29
35
|
- run: uv python install ${{ matrix.python }}
|
|
30
36
|
- run: uv sync --python ${{ matrix.python }} --group dev
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
ci:
|
|
9
|
+
uses: ./.github/workflows/ci.yml
|
|
10
|
+
secrets: inherit
|
|
11
|
+
|
|
12
|
+
publish:
|
|
13
|
+
needs: ci
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
- uses: astral-sh/setup-uv@v6
|
|
23
|
+
- run: uv build
|
|
24
|
+
- name: Publish to PyPI
|
|
25
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -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
|
|
|
@@ -40,10 +42,13 @@ Any mutable state needed across tests or across a test run must be held as
|
|
|
40
42
|
class attributes or instance attributes — never as module-level variables.
|
|
41
43
|
|
|
42
44
|
```python
|
|
45
|
+
import threading
|
|
46
|
+
|
|
43
47
|
# Good
|
|
44
48
|
class TestState:
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
log = {}
|
|
50
|
+
barrier = threading.Barrier(3, timeout=10)
|
|
51
|
+
|
|
47
52
|
|
|
48
53
|
# Bad
|
|
49
54
|
_log = {}
|
|
@@ -67,9 +72,11 @@ subprocess with the exact thread count needed. Use explicit thread counts for
|
|
|
67
72
|
barrier-based tests (not `auto`) to avoid deadlocks on low-core machines.
|
|
68
73
|
|
|
69
74
|
```python
|
|
75
|
+
import threading
|
|
76
|
+
|
|
70
77
|
# Good — isolated subprocess via ftdir, explicit thread count
|
|
71
78
|
def test_children_run_concurrently(self, ftdir):
|
|
72
|
-
|
|
79
|
+
ftdir.makepyfile("""
|
|
73
80
|
import threading, pytest
|
|
74
81
|
@pytest.mark.parallelizable("children")
|
|
75
82
|
class TestBarrier:
|
|
@@ -77,16 +84,25 @@ def test_children_run_concurrently(self, ftdir):
|
|
|
77
84
|
def test_a(self): self.barrier.wait()
|
|
78
85
|
def test_b(self): self.barrier.wait()
|
|
79
86
|
""")
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
result = ftdir.run_pytest("--threadpool", "2")
|
|
88
|
+
result.assert_outcomes(passed=2)
|
|
89
|
+
|
|
82
90
|
|
|
83
91
|
# Bad — depends on outer runner flags
|
|
84
92
|
class TestBarrier:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
barrier = threading.Barrier(2, timeout=10)
|
|
94
|
+
|
|
95
|
+
def test_a(self): self.barrier.wait()
|
|
96
|
+
|
|
97
|
+
def test_b(self): self.barrier.wait()
|
|
88
98
|
```
|
|
89
99
|
|
|
100
|
+
### Use `pytest.fail()` for intentional failures
|
|
101
|
+
|
|
102
|
+
In test cases that need to intentionally fail (e.g., verifying teardown after
|
|
103
|
+
failure), use `pytest.fail("reason")` instead of `assert False, "reason"`.
|
|
104
|
+
The latter triggers ruff PT015 and B011.
|
|
105
|
+
|
|
90
106
|
### Constants over strings
|
|
91
107
|
|
|
92
108
|
Use `ParallelScope` enum and `_constants` module instead of bare string
|
|
@@ -128,18 +144,19 @@ Every commit message starts with one or more tags in brackets:
|
|
|
128
144
|
|
|
129
145
|
### Tags
|
|
130
146
|
|
|
131
|
-
| Tag
|
|
132
|
-
|
|
133
|
-
| `[Runner]`
|
|
134
|
-
| `[Markers]`
|
|
135
|
-
| `[Report]`
|
|
136
|
-
| `[Plugin]`
|
|
137
|
-
| `[API]`
|
|
138
|
-
| `[Test]`
|
|
139
|
-
| `[
|
|
140
|
-
| `[
|
|
141
|
-
| `[
|
|
142
|
-
| `[
|
|
147
|
+
| Tag | Scope |
|
|
148
|
+
|--------------|---------------------------------------------------------------------------------|
|
|
149
|
+
| `[Runner]` | Core parallel execution engine (`_runner.py`, `_fixtures.py`) |
|
|
150
|
+
| `[Markers]` | Marker resolution and grouping (`_markers.py`, `_grouping.py`, `_constants.py`) |
|
|
151
|
+
| `[Report]` | Terminal reporting and display (`_LiveReporter`) |
|
|
152
|
+
| `[Plugin]` | Plugin hooks and CLI options (`plugin.py`) |
|
|
153
|
+
| `[API]` | Public API changes (`_api.py`, `__init__.py`) |
|
|
154
|
+
| `[Test]` | Test additions or changes only |
|
|
155
|
+
| `[Build]` | CI/CD pipelines, publishing, versioning |
|
|
156
|
+
| `[Tooling]` | Ruff, pyright, pre-commit, scripts |
|
|
157
|
+
| `[Docs]` | README, CONTRIBUTING, docstrings |
|
|
158
|
+
| `[Refactor]` | Internal restructuring, no behavior change |
|
|
159
|
+
| `[Fix]` | Bug fix — must include issue number if fixing a known issue |
|
|
143
160
|
|
|
144
161
|
### Combining tags
|
|
145
162
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-threadpool
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Parallel test execution for free-threaded Python builds
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -10,26 +10,32 @@ Classifier: Framework :: Pytest
|
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
13
16
|
Classifier: Topic :: Software Development :: Testing
|
|
14
17
|
Requires-Python: >=3.13
|
|
15
18
|
Requires-Dist: pytest>=9.0.2
|
|
16
|
-
Requires-Dist: selenium>=4.41.0
|
|
17
19
|
Description-Content-Type: text/markdown
|
|
18
20
|
|
|
19
21
|
# pytest-threadpool
|
|
20
22
|
|
|
21
|
-

|
|
22
|
-
](https://pypi.org/project/pytest-threadpool/)
|
|
24
|
+
[](https://pypi.org/project/pytest-threadpool/)
|
|
23
25
|
[](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
|
|
24
26
|
[](https://github.com/pytest-threadpool/pytest-threadpool)
|
|
25
27
|
[](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml)
|
|
26
28
|
[](https://codecov.io/gh/pytest-threadpool/pytest-threadpool)
|
|
27
29
|
|
|
28
|
-
**Status: Beta** · Parallel test execution
|
|
30
|
+
**Status: Beta** · Parallel test execution using threads.
|
|
29
31
|
|
|
30
|
-
Runs test *bodies
|
|
31
|
-
pool while keeping shared fixtures
|
|
32
|
-
sequential.
|
|
32
|
+
Runs test *bodies*, function-scoped fixture setup, and function-scoped fixture
|
|
33
|
+
teardown concurrently in a thread pool while keeping shared fixtures
|
|
34
|
+
(module/class/session scope) sequential.
|
|
35
|
+
|
|
36
|
+
Works on any Python 3.13+. Free-threaded builds (3.13t, 3.14t, 3.15t) get true
|
|
37
|
+
parallelism for CPU-bound tests. Standard builds still benefit from parallel
|
|
38
|
+
execution of I/O-bound tests (network, database, file operations).
|
|
33
39
|
|
|
34
40
|
## Installation
|
|
35
41
|
|
|
@@ -88,12 +94,12 @@ sequentially before workers launch and served from cache to all items.
|
|
|
88
94
|
|
|
89
95
|
| Scope | Behavior |
|
|
90
96
|
|------------|-------------------------------------------------|
|
|
91
|
-
| `function` | Cloned per-item,
|
|
92
|
-
| `class` | Resolved once, cached, shared across items
|
|
93
|
-
| `module` | Resolved once, cached, shared across items
|
|
94
|
-
| `session` | Resolved once, cached, shared across items
|
|
97
|
+
| `function` | Cloned per-item, setup and teardown in parallel workers |
|
|
98
|
+
| `class` | Resolved once, cached, shared across items |
|
|
99
|
+
| `module` | Resolved once, cached, shared across items |
|
|
100
|
+
| `session` | Resolved once, cached, shared across items |
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
Shared fixture teardown runs sequentially after the parallel group completes.
|
|
97
103
|
|
|
98
104
|
## Marker levels
|
|
99
105
|
|
|
@@ -168,9 +174,12 @@ pytest
|
|
|
168
174
|
|
|
169
175
|
| Component | Versions |
|
|
170
176
|
|-----------|----------|
|
|
171
|
-
| Python | 3.13t, 3.14t, 3.15t |
|
|
177
|
+
| Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
|
|
172
178
|
| pytest | >=9.0.2 |
|
|
173
179
|
|
|
180
|
+
> **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
|
|
181
|
+
> for CPU-bound tests. I/O-bound tests still run concurrently.
|
|
182
|
+
|
|
174
183
|
## Known limitations
|
|
175
184
|
|
|
176
185
|
- **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
# pytest-threadpool
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-
](https://pypi.org/project/pytest-threadpool/)
|
|
4
|
+
[](https://pypi.org/project/pytest-threadpool/)
|
|
5
5
|
[](https://github.com/pytest-threadpool/pytest-threadpool/blob/main/LICENSE)
|
|
6
6
|
[](https://github.com/pytest-threadpool/pytest-threadpool)
|
|
7
7
|
[](https://github.com/pytest-threadpool/pytest-threadpool/actions/workflows/ci.yml)
|
|
8
8
|
[](https://codecov.io/gh/pytest-threadpool/pytest-threadpool)
|
|
9
9
|
|
|
10
|
-
**Status: Beta** · Parallel test execution
|
|
10
|
+
**Status: Beta** · Parallel test execution using threads.
|
|
11
11
|
|
|
12
|
-
Runs test *bodies
|
|
13
|
-
pool while keeping shared fixtures
|
|
14
|
-
sequential.
|
|
12
|
+
Runs test *bodies*, function-scoped fixture setup, and function-scoped fixture
|
|
13
|
+
teardown concurrently in a thread pool while keeping shared fixtures
|
|
14
|
+
(module/class/session scope) sequential.
|
|
15
|
+
|
|
16
|
+
Works on any Python 3.13+. Free-threaded builds (3.13t, 3.14t, 3.15t) get true
|
|
17
|
+
parallelism for CPU-bound tests. Standard builds still benefit from parallel
|
|
18
|
+
execution of I/O-bound tests (network, database, file operations).
|
|
15
19
|
|
|
16
20
|
## Installation
|
|
17
21
|
|
|
@@ -70,12 +74,12 @@ sequentially before workers launch and served from cache to all items.
|
|
|
70
74
|
|
|
71
75
|
| Scope | Behavior |
|
|
72
76
|
|------------|-------------------------------------------------|
|
|
73
|
-
| `function` | Cloned per-item,
|
|
74
|
-
| `class` | Resolved once, cached, shared across items
|
|
75
|
-
| `module` | Resolved once, cached, shared across items
|
|
76
|
-
| `session` | Resolved once, cached, shared across items
|
|
77
|
+
| `function` | Cloned per-item, setup and teardown in parallel workers |
|
|
78
|
+
| `class` | Resolved once, cached, shared across items |
|
|
79
|
+
| `module` | Resolved once, cached, shared across items |
|
|
80
|
+
| `session` | Resolved once, cached, shared across items |
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
Shared fixture teardown runs sequentially after the parallel group completes.
|
|
79
83
|
|
|
80
84
|
## Marker levels
|
|
81
85
|
|
|
@@ -150,9 +154,12 @@ pytest
|
|
|
150
154
|
|
|
151
155
|
| Component | Versions |
|
|
152
156
|
|-----------|----------|
|
|
153
|
-
| Python | 3.13t, 3.14t, 3.15t |
|
|
157
|
+
| Python | 3.13, 3.13t, 3.14, 3.14t, 3.15, 3.15t |
|
|
154
158
|
| pytest | >=9.0.2 |
|
|
155
159
|
|
|
160
|
+
> **Note:** On standard (GIL-enabled) builds, the GIL limits parallel speedup
|
|
161
|
+
> for CPU-bound tests. I/O-bound tests still run concurrently.
|
|
162
|
+
|
|
156
163
|
## Known limitations
|
|
157
164
|
|
|
158
165
|
- **Private pytest API usage** — The plugin relies on internal `_pytest` APIs
|
|
@@ -0,0 +1 @@
|
|
|
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()
|
|
@@ -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)
|