././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1768384515.889737 pytest_run_parallel-0.8.2/0000755000175100017510000000000015131664004015257 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/.pre-commit-config.yaml0000644000175100017510000000113315131663774021553 0ustar00runnerrunnerrepos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell additional_dependencies: - tomli - repo: https://github.com/rstcheck/rstcheck rev: v6.2.5 hooks: - id: rstcheck additional_dependencies: ["sphinx"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.1 hooks: - id: ruff-check args: [ --fix ] - id: ruff-format ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/LICENSE0000644000175100017510000000205715131663774016305 0ustar00runnerrunnerMIT License Copyright (c) 2024 Quansight Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/MANIFEST.in0000644000175100017510000000041615131663774017033 0ustar00runnerrunnerinclude tests/conftest.py exclude pytest_run_parallel.egg-info recursive-exclude * *.egg-info recursive-include docs *.bat recursive-include docs *.rst recursive-include docs Makefile include *.lock include *.md include *.toml include *.yaml recursive-include docs *.py ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1768384515.889737 pytest_run_parallel-0.8.2/PKG-INFO0000644000175100017510000004057715131664004016371 0ustar00runnerrunnerMetadata-Version: 2.4 Name: pytest-run-parallel Version: 0.8.2 Summary: A simple pytest plugin to run tests concurrently Author: Edgar Margffoy Maintainer-email: Lysandros Nikolaou , Nathan Goldbaum License-Expression: MIT Project-URL: Repository, https://github.com/Quansight-Labs/pytest-run-parallel Classifier: Framework :: Pytest Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Testing Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=3.9 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: pytest>=6.2.0 Provides-Extra: psutil Requires-Dist: psutil>=6.1.1; extra == "psutil" Dynamic: license-file # pytest-run-parallel [![PyPI version](https://img.shields.io/pypi/v/pytest-run-parallel.svg)](https://pypi.org/project/pytest-run-parallel) [![Python versions](https://img.shields.io/pypi/pyversions/pytest-run-parallel.svg)](https://pypi.org/project/pytest-run-parallel) [![See Build Status on GitHub Actions](https://github.com/Quansight-Labs/pytest-run-parallel/actions/workflows/main.yml/badge.svg)](https://github.com/Quansight-Labs/pytest-run-parallel/actions/workflows/main.yml) A simple pytest plugin to run tests concurrently ------------------------------------------------------------------------ This [pytest](https://github.com/pytest-dev/pytest) plugin takes a set of tests that would be normally be run serially and execute them in parallel. The main goal of `pytest-run-parallel` is to discover thread-safety issues that could exist when using C libraries, this is of vital importance after [PEP703](https://peps.python.org/pep-0703/), which provides a path for a CPython implementation without depending on the Global Interpreter Lock (GIL), thus allowing for proper parallelism in programs that make use of the CPython interpreter. For more information about C thread-safety issues, please visit the free-threaded community guide at ## How it works This plugin is *not* an alternative to [pytest-xdist](https://pytest-xdist.readthedocs.io/) and does not run all of the tests in a test suite simultaneously in a thread pool. Instead, it runs many instances of the same test in a thread pool. It is only useful as a tool to do multithreaded stress tests using an existing test suite and is not useful to speed up the execution of a test suite via multithreaded parallelism. Given an existing test taking arguments `*args` and keyword arguments `**kwargs`, this plugin creates a new test that is equivalent to the following Python code: ```python import threading from concurrent.futures import ThreadPoolExecutor def run_test(b, *args, **kwargs): for _ in range(num_iterations): b.wait() execute_pytest_test(*args, **kwargs) with ThreadPoolExecutor(max_workers=num_parallel_threads) as tpe: b = threading.Barrier(num_parallel_threads) for _ in range(num_parallel_threads): tpe.submit(run_test, b, *args, **kwargs) ``` The `execute_pytest_test` function hides some magic to ensure errors and failures get propagated correctly to the main testing thread. Using this plugin avoids the boilerplate of rewriting existing tests to run in parallel in a thread pool. Note that `args` and `kwargs` might include pytest marks and fixtures, and the way this plugin is currently written, those fixtures are shared between threads. ## Features - Global CLI flags: - `--parallel-threads` to run a test suite in parallel - `--iterations` to run multiple times in each thread - `--skip-thread-unsafe` to skip running tests marked as or detected to be thread-unsafe. - `--mark-warnings-as-unsafe` and `--mark-ctypes-as-unsafe` to always skip running tests that use the `warnings` or `ctypes` modules, respectively. These are useful if you are adding support for Python 3.14 to a library that already runs tests under pytest-run-parallel on Python 3.13 or older. - `--mark-hypothesis-as-unsafe` to always skip running tests that use [hypothesis](https://github.com/hypothesisworks/hypothesis). While newer version of Hypothesis are thread-safe, and versions which are not are automatically skipped by `pytest-run-parallel`, this flag is an escape hatch in case you run into thread-safety problems caused by Hypothesis, or in tests that happen to use hypothesis and were skipped in older versions of pytest-run-parallel. - `--forever` to keep running tests in an endless loop starting from the top when completing a test run, until a test crashes or the user explicitly ends the process with Ctrl-C. This is especially helpful when trying to reproduce thread safety bugs that might only occur rarely. Note that pytest's progress indicator will keep showing 100% forever after the first pass of the test suite. `forever` is not compatible with `-n` from [pytest-xdist](https://github.com/pytest-dev/pytest-xdist). - `--ignore-gil-enabled` to ignore the RuntimeWarning generated when the GIL is enabled at runtime on the free-threaded build and run the tests despite the fact that the GIL is enabled. This option has no effect if pytest is configured to treat warnings as errors. - Four corresponding markers: - `pytest.mark.parallel_threads_limit(n)` to mark a single test to run in a maximum of `n` threads, even if the `--parallel-threads` command-line argument is set to a higher value. This is useful if a test uses resources that should be limited. - `pytest.mark.parallel_threads(n)` to mark a test to always run in `n` threads. Note that this implies that the test will be multi-threaded just because pytest-run-parallel is installed, even if `--parallel-threads` is not passed at the command-line. - `pytest.mark.thread_unsafe` to mark a single test to run in a single thread. It is equivalent to using `pytest.mark.parallel_threads(1)` - `pytest.mark.iterations(n)` to mark a single test to run `n` times in each thread - Four corresponding fixtures: - `num_parallel_threads`: The number of threads the test will run in - `num_iterations`: The number of iterations the test will run in each thread - `thread_index`: An index for the test's current thread. - `iteration_index`: An index for the test's current iteration. - Modifications to existing fixtures: - `tmp_path` and `tmpdir`: Patched to be thread-safe, with individual subdirectories being created for each thread. Subdirectories are not created for each iteration, so you may see issues with reused temporary directions when using `--iterations`. While pytest-run-parallel has special handling for the `tmp_path` and `tmpdir` fixtures to ensure that each thread has a private temporary directory, the plugin only does this if a test requests these fixtures directly. If another fixture requests `tmp_path` or `tmpdir`, then all threads will share a temporary directory in that fixture. When using the fixtures `thread_index` and `iteration_index`, they should be requested directly by tests, and will return 0 when requested by other fixtures. **Note**: It's possible to specify `--parallel-threads=auto` or `pytest.mark.parallel_threads("auto")` which will let `pytest-run-parallel` choose the number of logical CPU cores available to the testing process. If that cannot be determined, the number of physical CPU cores will be used. If that fails as well, it will fall back to running all tests single-threaded. ## Requirements `pytest-run-parallel` depends exclusively on `pytest`. Optionally installing `psutil` will help with identifying the number of logical cores available to the testing process in systems where that's not possible with the Python stdlib. ## Installation You can install "pytest-run-parallel" via [pip](https://pypi.org/project/pip/) from [PyPI](https://pypi.org/project): $ pip install pytest-run-parallel If you want to additionally install `psutil` you can run: $ pip install pytest-run-parallel[psutil] ## Caveats Pytest itself is not thread-safe and it is not safe to share stateful pytest fixtures or marks between threads. Existing tests relying on setting up mutable state via a fixture will see the state shared between threads. Tests that dynamically set marks or share marks will also likely not be thread-safe. See the pytest documentation [for more detail](https://docs.pytest.org/en/stable/explanation/flaky.html#thread-safety) and the community-maintained [free threaded Python porting guide](https://py-free-threading.github.io/porting/#pytest-is-not-thread-safe) for more detail about using pytest in a multithreaded context on the free-threaded build of Python. We suggest marking tests that are incompatible with this plugin's current design with `@pytest.mark.thread_unsafe` or `@pytest.mark.thread_unsafe(reason="...")`. The following functions and modules are known to be thread-unsafe and pytest-run-parallel will automatically skip running tests using them in parallel: - The pytest `capsys`, `capteesys`, `capsysbinary`, `capfd` and `capfdbinary` fixtures - The pytest `monkeypath` fixture The following fixtures are known to be thread-unsafe on Python 3.13 and older, or on 3.14 and newer if Python isn't configured correctly: - `pytest.warns` - `pytest.deprecated_call` - The pytest `recwarn` fixture - `warnings.catch_warnings` - `unittest.mock` - `ctypes` If an older version of `hypothesis` that is known to be thread-unsafe is installed, tests using `hypothesis` are skipped. Additionally, if a set of fixtures is known to be thread unsafe, tests that use them can be automatically marked as thread unsafe by declaring them under the thread_unsafe_fixtures option under pytest INI configuration file: ```ini [pytest] thread_unsafe_fixtures = fixture_1 fixture_2 ... ``` Or under the section `tool.pytest.ini_options` if using `pyproject.toml`: ```toml [tool.pytest.ini_options] thread_unsafe_fixtures = [ 'fixture_1', 'fixture_2', ... ] ``` Similarly, if a function is known to be thread unsafe and should cause a test to be marked as thread-unsafe as well, its fully-qualified name can be registered through the `thread_unsafe_functions` option in the INI file (or under `tool.pytest.ini_options` when using `pyproject.toml`): ```ini [pytest] thread_unsafe_functions = module.submodule.func1 module.submodule2.func2 ... ``` You can also blocklist entire modules by using an asterisk: ```ini [pytest] thread_unsafe_functions = module1.* module2.submodule.* ... ``` Also, if you define a `__thread_safe__ = False` attribute on a function that is called by a test and is up to two levels below in the call stack, then pytest-run-parallel will automatically detect that a thread-unsafe function is being used and will mark the test as thread-unsafe. ## Usage This plugin has two modes of operation, one via the `--parallel-threads` and `--iterations` pytest CLI flags, which allows a whole test suite to be run in parallel: $ pytest --parallel-threads=10 --iterations=10 tests By default, the value for both flags will be 1, thus not modifying the usual behaviour of pytest except when the flag is set. Note that using `pytest-xdist` and setting `iterations` to a number greater than one might cause tests to run even more times than intended. The other mode of operation occurs at the individual test level, via the `pytest.mark.parallel_threads` and `pytest.mark.iterations` markers: ```python # test_file.py import pytest @pytest.fixture def my_fixture(): ... @pytest.mark.parallel_threads(2) @pytest.mark.iterations(10) def test_something_1(): # This test will be run in parallel using two concurrent threads # and 10 times in each thread ... @pytest.mark.parametrize('arg', [1, 2, 3]) @pytest.mark.parallel_threads(3) def test_fixture(my_fixture, arg): # pytest markers and fixtures are supported as well ... ``` Both modes of operations are supported simultaneously, i.e., ```bash # test_something_1 and test_fixture will be run using their set number of # threads; other tests will be run using 5 threads. $ pytest -x -v --parallel-threads=5 test_file.py ``` You can skip tests marked as or detected to be thread-unsafe by passing `--skip-thread-unsafe` in your pytest invocation. This is useful when running pytest-run-parallel under [Thread Sanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html). Setting `--skip-thread-unsafe=True` will avoid unnecessarily running tests where thread sanitizer cannot detect races because the test is not parallelized. Older versions of pytest-run-parallel always marked tests using the `warnings` and `ctypes` modules as thread-unsafe, since both were not thread-safe until Python 3.14. If you are adding support for Python 3.14 and would like to continue marking tests that use `warnings` or `ctypes`, pass `--mark-warnings-as-unsafe` or `--mark-ctypes-as-unsafe`, respectively, in your `pytest` invocation. Additionally, `pytest-run-parallel` exposes the `num_parallel_threads` and `num_iterations` fixtures which enable a test to be aware of the number of threads that are being spawned and the number of iterations each test will run: ```python # test_file.py import pytest def test_skip_if_parallel(num_parallel_threads): if num_parallel_threads > 1: pytest.skip(reason='does not work in parallel') ... ``` The `thread_index` and `iteration_index` fixtures are also available, which enable tests to display different behavior between threads and iterations. ```python # test_file.py import numpy as np def test_unique_rng_streams(thread_index): # create an RNG stream with a seed that is deterministic # but still unique to this thread rng = np.random.default_rng(thread_index) ... ``` Finally, the `thread_comp` fixture allows for parallel test debugging, by providing an instance of `ThreadComparator`, whose `__call__` method allows to check if all the values produced by all threads during an specific execution step are the same: ``` python # test_file.py def test_same_execution_values(thread_comp): a = 2 b = [3, 4, 5] c = None # Check that the values for a, b, c are the same across tests thread_comp(a=a, b=b, c=c) ``` ## Tracing If you run pytest with verbose output (e.g. by passing `-v` in your pytest invocation), you will see that tests are annotated to either "PASS" or "PARALLEL PASS". A "PASS" indicates the test was run on a single thread, whereas "PARALLEL PASS" indicates the test passed and was run in a thread pool. If a test was not run in a thread pool because pytest-run-parallel detected use of thread-unsafe functionality, the reason will be printed as well. If you are running pytest in the default configuration without `-v`, then tests that pass in a thread pool will be annotated with a slightly different dot character, allowing you to visually pick out when tests are not run in parallel. For example in the output for this file: ``` tests/test_kx.py ·....· ``` Only the first and last tests are run in parallel. In order to list the tests that were marked as thread-unsafe and were not executed in parallel, you can set the `PYTEST_RUN_PARALLEL_VERBOSE` environment variable to 1. ## Contributing Contributions are very welcome. Tests can be run with `uv run pytest`. If you want to run `pytest` with coverage enabled you can run `uv run coverage run` and then `uv run coverage report -m` to get the coverage report. Please ensure the coverage at least stays the same before you submit a pull request. ## License Distributed under the terms of the [MIT](https://opensource.org/licenses/MIT) license, "pytest-run-parallel" is free and open source software ## Issues If you encounter any problems, please [file an issue](https://github.com/Quansight-Labs/pytest-run-parallel/issues) along with a detailed description. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/README.md0000644000175100017510000003612215131663774016557 0ustar00runnerrunner# pytest-run-parallel [![PyPI version](https://img.shields.io/pypi/v/pytest-run-parallel.svg)](https://pypi.org/project/pytest-run-parallel) [![Python versions](https://img.shields.io/pypi/pyversions/pytest-run-parallel.svg)](https://pypi.org/project/pytest-run-parallel) [![See Build Status on GitHub Actions](https://github.com/Quansight-Labs/pytest-run-parallel/actions/workflows/main.yml/badge.svg)](https://github.com/Quansight-Labs/pytest-run-parallel/actions/workflows/main.yml) A simple pytest plugin to run tests concurrently ------------------------------------------------------------------------ This [pytest](https://github.com/pytest-dev/pytest) plugin takes a set of tests that would be normally be run serially and execute them in parallel. The main goal of `pytest-run-parallel` is to discover thread-safety issues that could exist when using C libraries, this is of vital importance after [PEP703](https://peps.python.org/pep-0703/), which provides a path for a CPython implementation without depending on the Global Interpreter Lock (GIL), thus allowing for proper parallelism in programs that make use of the CPython interpreter. For more information about C thread-safety issues, please visit the free-threaded community guide at ## How it works This plugin is *not* an alternative to [pytest-xdist](https://pytest-xdist.readthedocs.io/) and does not run all of the tests in a test suite simultaneously in a thread pool. Instead, it runs many instances of the same test in a thread pool. It is only useful as a tool to do multithreaded stress tests using an existing test suite and is not useful to speed up the execution of a test suite via multithreaded parallelism. Given an existing test taking arguments `*args` and keyword arguments `**kwargs`, this plugin creates a new test that is equivalent to the following Python code: ```python import threading from concurrent.futures import ThreadPoolExecutor def run_test(b, *args, **kwargs): for _ in range(num_iterations): b.wait() execute_pytest_test(*args, **kwargs) with ThreadPoolExecutor(max_workers=num_parallel_threads) as tpe: b = threading.Barrier(num_parallel_threads) for _ in range(num_parallel_threads): tpe.submit(run_test, b, *args, **kwargs) ``` The `execute_pytest_test` function hides some magic to ensure errors and failures get propagated correctly to the main testing thread. Using this plugin avoids the boilerplate of rewriting existing tests to run in parallel in a thread pool. Note that `args` and `kwargs` might include pytest marks and fixtures, and the way this plugin is currently written, those fixtures are shared between threads. ## Features - Global CLI flags: - `--parallel-threads` to run a test suite in parallel - `--iterations` to run multiple times in each thread - `--skip-thread-unsafe` to skip running tests marked as or detected to be thread-unsafe. - `--mark-warnings-as-unsafe` and `--mark-ctypes-as-unsafe` to always skip running tests that use the `warnings` or `ctypes` modules, respectively. These are useful if you are adding support for Python 3.14 to a library that already runs tests under pytest-run-parallel on Python 3.13 or older. - `--mark-hypothesis-as-unsafe` to always skip running tests that use [hypothesis](https://github.com/hypothesisworks/hypothesis). While newer version of Hypothesis are thread-safe, and versions which are not are automatically skipped by `pytest-run-parallel`, this flag is an escape hatch in case you run into thread-safety problems caused by Hypothesis, or in tests that happen to use hypothesis and were skipped in older versions of pytest-run-parallel. - `--forever` to keep running tests in an endless loop starting from the top when completing a test run, until a test crashes or the user explicitly ends the process with Ctrl-C. This is especially helpful when trying to reproduce thread safety bugs that might only occur rarely. Note that pytest's progress indicator will keep showing 100% forever after the first pass of the test suite. `forever` is not compatible with `-n` from [pytest-xdist](https://github.com/pytest-dev/pytest-xdist). - `--ignore-gil-enabled` to ignore the RuntimeWarning generated when the GIL is enabled at runtime on the free-threaded build and run the tests despite the fact that the GIL is enabled. This option has no effect if pytest is configured to treat warnings as errors. - Four corresponding markers: - `pytest.mark.parallel_threads_limit(n)` to mark a single test to run in a maximum of `n` threads, even if the `--parallel-threads` command-line argument is set to a higher value. This is useful if a test uses resources that should be limited. - `pytest.mark.parallel_threads(n)` to mark a test to always run in `n` threads. Note that this implies that the test will be multi-threaded just because pytest-run-parallel is installed, even if `--parallel-threads` is not passed at the command-line. - `pytest.mark.thread_unsafe` to mark a single test to run in a single thread. It is equivalent to using `pytest.mark.parallel_threads(1)` - `pytest.mark.iterations(n)` to mark a single test to run `n` times in each thread - Four corresponding fixtures: - `num_parallel_threads`: The number of threads the test will run in - `num_iterations`: The number of iterations the test will run in each thread - `thread_index`: An index for the test's current thread. - `iteration_index`: An index for the test's current iteration. - Modifications to existing fixtures: - `tmp_path` and `tmpdir`: Patched to be thread-safe, with individual subdirectories being created for each thread. Subdirectories are not created for each iteration, so you may see issues with reused temporary directions when using `--iterations`. While pytest-run-parallel has special handling for the `tmp_path` and `tmpdir` fixtures to ensure that each thread has a private temporary directory, the plugin only does this if a test requests these fixtures directly. If another fixture requests `tmp_path` or `tmpdir`, then all threads will share a temporary directory in that fixture. When using the fixtures `thread_index` and `iteration_index`, they should be requested directly by tests, and will return 0 when requested by other fixtures. **Note**: It's possible to specify `--parallel-threads=auto` or `pytest.mark.parallel_threads("auto")` which will let `pytest-run-parallel` choose the number of logical CPU cores available to the testing process. If that cannot be determined, the number of physical CPU cores will be used. If that fails as well, it will fall back to running all tests single-threaded. ## Requirements `pytest-run-parallel` depends exclusively on `pytest`. Optionally installing `psutil` will help with identifying the number of logical cores available to the testing process in systems where that's not possible with the Python stdlib. ## Installation You can install "pytest-run-parallel" via [pip](https://pypi.org/project/pip/) from [PyPI](https://pypi.org/project): $ pip install pytest-run-parallel If you want to additionally install `psutil` you can run: $ pip install pytest-run-parallel[psutil] ## Caveats Pytest itself is not thread-safe and it is not safe to share stateful pytest fixtures or marks between threads. Existing tests relying on setting up mutable state via a fixture will see the state shared between threads. Tests that dynamically set marks or share marks will also likely not be thread-safe. See the pytest documentation [for more detail](https://docs.pytest.org/en/stable/explanation/flaky.html#thread-safety) and the community-maintained [free threaded Python porting guide](https://py-free-threading.github.io/porting/#pytest-is-not-thread-safe) for more detail about using pytest in a multithreaded context on the free-threaded build of Python. We suggest marking tests that are incompatible with this plugin's current design with `@pytest.mark.thread_unsafe` or `@pytest.mark.thread_unsafe(reason="...")`. The following functions and modules are known to be thread-unsafe and pytest-run-parallel will automatically skip running tests using them in parallel: - The pytest `capsys`, `capteesys`, `capsysbinary`, `capfd` and `capfdbinary` fixtures - The pytest `monkeypath` fixture The following fixtures are known to be thread-unsafe on Python 3.13 and older, or on 3.14 and newer if Python isn't configured correctly: - `pytest.warns` - `pytest.deprecated_call` - The pytest `recwarn` fixture - `warnings.catch_warnings` - `unittest.mock` - `ctypes` If an older version of `hypothesis` that is known to be thread-unsafe is installed, tests using `hypothesis` are skipped. Additionally, if a set of fixtures is known to be thread unsafe, tests that use them can be automatically marked as thread unsafe by declaring them under the thread_unsafe_fixtures option under pytest INI configuration file: ```ini [pytest] thread_unsafe_fixtures = fixture_1 fixture_2 ... ``` Or under the section `tool.pytest.ini_options` if using `pyproject.toml`: ```toml [tool.pytest.ini_options] thread_unsafe_fixtures = [ 'fixture_1', 'fixture_2', ... ] ``` Similarly, if a function is known to be thread unsafe and should cause a test to be marked as thread-unsafe as well, its fully-qualified name can be registered through the `thread_unsafe_functions` option in the INI file (or under `tool.pytest.ini_options` when using `pyproject.toml`): ```ini [pytest] thread_unsafe_functions = module.submodule.func1 module.submodule2.func2 ... ``` You can also blocklist entire modules by using an asterisk: ```ini [pytest] thread_unsafe_functions = module1.* module2.submodule.* ... ``` Also, if you define a `__thread_safe__ = False` attribute on a function that is called by a test and is up to two levels below in the call stack, then pytest-run-parallel will automatically detect that a thread-unsafe function is being used and will mark the test as thread-unsafe. ## Usage This plugin has two modes of operation, one via the `--parallel-threads` and `--iterations` pytest CLI flags, which allows a whole test suite to be run in parallel: $ pytest --parallel-threads=10 --iterations=10 tests By default, the value for both flags will be 1, thus not modifying the usual behaviour of pytest except when the flag is set. Note that using `pytest-xdist` and setting `iterations` to a number greater than one might cause tests to run even more times than intended. The other mode of operation occurs at the individual test level, via the `pytest.mark.parallel_threads` and `pytest.mark.iterations` markers: ```python # test_file.py import pytest @pytest.fixture def my_fixture(): ... @pytest.mark.parallel_threads(2) @pytest.mark.iterations(10) def test_something_1(): # This test will be run in parallel using two concurrent threads # and 10 times in each thread ... @pytest.mark.parametrize('arg', [1, 2, 3]) @pytest.mark.parallel_threads(3) def test_fixture(my_fixture, arg): # pytest markers and fixtures are supported as well ... ``` Both modes of operations are supported simultaneously, i.e., ```bash # test_something_1 and test_fixture will be run using their set number of # threads; other tests will be run using 5 threads. $ pytest -x -v --parallel-threads=5 test_file.py ``` You can skip tests marked as or detected to be thread-unsafe by passing `--skip-thread-unsafe` in your pytest invocation. This is useful when running pytest-run-parallel under [Thread Sanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html). Setting `--skip-thread-unsafe=True` will avoid unnecessarily running tests where thread sanitizer cannot detect races because the test is not parallelized. Older versions of pytest-run-parallel always marked tests using the `warnings` and `ctypes` modules as thread-unsafe, since both were not thread-safe until Python 3.14. If you are adding support for Python 3.14 and would like to continue marking tests that use `warnings` or `ctypes`, pass `--mark-warnings-as-unsafe` or `--mark-ctypes-as-unsafe`, respectively, in your `pytest` invocation. Additionally, `pytest-run-parallel` exposes the `num_parallel_threads` and `num_iterations` fixtures which enable a test to be aware of the number of threads that are being spawned and the number of iterations each test will run: ```python # test_file.py import pytest def test_skip_if_parallel(num_parallel_threads): if num_parallel_threads > 1: pytest.skip(reason='does not work in parallel') ... ``` The `thread_index` and `iteration_index` fixtures are also available, which enable tests to display different behavior between threads and iterations. ```python # test_file.py import numpy as np def test_unique_rng_streams(thread_index): # create an RNG stream with a seed that is deterministic # but still unique to this thread rng = np.random.default_rng(thread_index) ... ``` Finally, the `thread_comp` fixture allows for parallel test debugging, by providing an instance of `ThreadComparator`, whose `__call__` method allows to check if all the values produced by all threads during an specific execution step are the same: ``` python # test_file.py def test_same_execution_values(thread_comp): a = 2 b = [3, 4, 5] c = None # Check that the values for a, b, c are the same across tests thread_comp(a=a, b=b, c=c) ``` ## Tracing If you run pytest with verbose output (e.g. by passing `-v` in your pytest invocation), you will see that tests are annotated to either "PASS" or "PARALLEL PASS". A "PASS" indicates the test was run on a single thread, whereas "PARALLEL PASS" indicates the test passed and was run in a thread pool. If a test was not run in a thread pool because pytest-run-parallel detected use of thread-unsafe functionality, the reason will be printed as well. If you are running pytest in the default configuration without `-v`, then tests that pass in a thread pool will be annotated with a slightly different dot character, allowing you to visually pick out when tests are not run in parallel. For example in the output for this file: ``` tests/test_kx.py ·....· ``` Only the first and last tests are run in parallel. In order to list the tests that were marked as thread-unsafe and were not executed in parallel, you can set the `PYTEST_RUN_PARALLEL_VERBOSE` environment variable to 1. ## Contributing Contributions are very welcome. Tests can be run with `uv run pytest`. If you want to run `pytest` with coverage enabled you can run `uv run coverage run` and then `uv run coverage report -m` to get the coverage report. Please ensure the coverage at least stays the same before you submit a pull request. ## License Distributed under the terms of the [MIT](https://opensource.org/licenses/MIT) license, "pytest-run-parallel" is free and open source software ## Issues If you encounter any problems, please [file an issue](https://github.com/Quansight-Labs/pytest-run-parallel/issues) along with a detailed description. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/RELEASE.md0000644000175100017510000000176515131663774016707 0ustar00runnerrunnerTo release a new version of `pytest-run-parallel`: 1. Update version in `pyproject.toml`, remove `-dev` suffix 2. Open PR with the version change 3. After PR is merged, run git tag -a vX.X.X -m "Release vX.X.X" 4. Create a new release in GitHub pointing to the new tag. Use autogenerated release notes. Edit to remove any unnecessary stuff like the release PRs. Wheels will be uploaded automatically by `.github/workflows/release.yml`. 5. Increment minor version and append the `-dev` suffix in `pyproject.toml` 6. Open PR with the version change 7. Monitor [conda-forge feedstock](https://github.com/conda-forge/pytest-run-parallel-feedstock) for anything that might be off in the auto-generated (and automerged) PR. For major releases consider the following manual testing steps before releasing 1. Run test collection for the following projects which use pytest-run-parallel in CI * SciPy * CFFI * pyyaml-ft The full tests are not necessary to run as most issues happen during AST parsing. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/pyproject.toml0000644000175100017510000000372115131663774020213 0ustar00runnerrunner[build-system] requires = [ "setuptools>=61.0.0", ] build-backend = "setuptools.build_meta" [project] name = "pytest-run-parallel" description = "A simple pytest plugin to run tests concurrently" version = "0.8.2" readme = "README.md" requires-python = ">=3.9" dependencies = [ "pytest>=6.2.0", ] authors = [ { name = "Edgar Margffoy" }, ] maintainers = [ { name = "Lysandros Nikolaou", email = "lnikolaou@quansight.com" }, { name = "Nathan Goldbaum", email = "ngoldbaum@quansight.com" }, ] license = "MIT" license-files = ["LICENSE"] classifiers = [ "Framework :: Pytest", "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Topic :: Software Development :: Testing", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] [project.urls] Repository = "https://github.com/Quansight-Labs/pytest-run-parallel" [project.entry-points.pytest11] run-parallel = "pytest_run_parallel.plugin" [project.optional-dependencies] psutil = [ "psutil>=6.1.1", ] [dependency-groups] test = [ "coverage[toml]>=7.8", "pytest>=8.4.0", "pytest-order", "pytest-xdist", "hypothesis>=6.135.33", "check-manifest", ] dev = [ { include-group = "test" }, "pre-commit>=3.5.0", "ruff>=0.7.2", ] [tool.ruff] exclude = ["docs/conf.py"] [tool.ruff.lint] select = ["E4", "E7", "E9", "F", "I"] [tool.pytest.ini_options] addopts="-W error -v" [tool.coverage.run] command_line = "-m pytest" source_pkgs = ["pytest_run_parallel"] source_dirs = ["tests"] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1768384515.889737 pytest_run_parallel-0.8.2/setup.cfg0000644000175100017510000000004615131664004017100 0ustar00runnerrunner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1768384515.884737 pytest_run_parallel-0.8.2/src/0000755000175100017510000000000015131664004016046 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1768384515.8867369 pytest_run_parallel-0.8.2/src/pytest_run_parallel/0000755000175100017510000000000015131664004022136 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel/__init__.py0000644000175100017510000000000015131663774024252 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel/cpu_detection.py0000644000175100017510000000150615131663774025354 0ustar00runnerrunnerdef get_logical_cpus(): try: import psutil except ImportError: pass else: process = psutil.Process() try: cpu_cores = process.cpu_affinity() return len(cpu_cores) except AttributeError: cpu_cores = psutil.cpu_count() if cpu_cores is not None: return cpu_cores try: from os import process_cpu_count except ImportError: pass else: cpu_cores = process_cpu_count() if cpu_cores is not None: return cpu_cores try: from os import sched_getaffinity except ImportError: pass else: cpu_cores = sched_getaffinity(0) if cpu_cores is not None: return len(cpu_cores) from os import cpu_count return cpu_count() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel/plugin.py0000644000175100017510000004700315131663774024027 0ustar00runnerrunnerimport functools import os import re import sys import threading import warnings import _pytest.doctest import _pytest.outcomes import pytest from pytest_run_parallel.thread_comparator import ThreadComparator from pytest_run_parallel.thread_unsafe_detection import ( construct_thread_unsafe_fixtures, identify_thread_unsafe_nodes, ) from pytest_run_parallel.utils import ( get_configured_num_workers, get_num_iterations, get_num_workers, ) GIL_WARNING_MESSAGE_CONTENT = re.compile( r"The global interpreter lock \(GIL\) has been enabled to load module '(?P[^']*)'" ) GIL_ENABLED_ERROR_TEXT = ( "GIL was dynamically re-enabled during test {stage_test} to load module '{module}'. " "When running under a free-threaded interpreter with the GIL initially disabled, " "the test suite must not cause the GIL to be re-enabled at runtime. Check " "for compiled extension modules that do not use the 'Py_mod_gil' slot or the " "'PyUnstable_Module_SetGIL' API. Pass --ignore-gil-enabled in your pytest invocation" "to ignore this and run the tests anyway." ) def wrap_function_parallel(fn, n_workers, n_iterations): @functools.wraps(fn) def inner(*args, **kwargs): errors = [] skip = None failed = None barrier = threading.Barrier(n_workers) original_switch = sys.getswitchinterval() new_switch = 1e-6 for _ in range(3): try: sys.setswitchinterval(new_switch) break except ValueError: new_switch *= 10 else: sys.setswitchinterval(original_switch) try: def closure(*args, **kwargs): # "smuggling" thread_index into closure with args thread_index, args = args[0], args[1:] # modifying fixtures if n_workers > 1: if "thread_index" in kwargs: kwargs["thread_index"] = thread_index if "tmp_path" in kwargs: kwargs["tmp_path"] = ( kwargs["tmp_path"] / f"thread_{thread_index!s}" ) kwargs["tmp_path"].mkdir(exist_ok=True) if "tmpdir" in kwargs: kwargs["tmpdir"] = kwargs["tmpdir"].ensure( f"thread_{thread_index!s}", dir=True ) for i in range(n_iterations): if "iteration_index" in kwargs: kwargs["iteration_index"] = i barrier.wait() try: fn(*args, **kwargs) except Warning: pass except Exception as e: errors.append(e) except _pytest.outcomes.Skipped as s: nonlocal skip skip = s.msg except _pytest.outcomes.Failed as f: nonlocal failed failed = f workers = [] for i in range(0, n_workers): worker_kwargs = kwargs # "smuggling" i into closure with args to use for thread_index fixture workers.append( threading.Thread( target=closure, args=(i, *args), kwargs=worker_kwargs ) ) num_completed = 0 try: for worker in workers: worker.start() num_completed += 1 finally: if num_completed < len(workers): barrier.abort() for worker in workers: worker.join() # if we ever want to add cleanup, put it here finally: sys.setswitchinterval(original_switch) if skip is not None: pytest.skip(skip) elif failed is not None: raise failed elif errors: raise errors[0] return inner class RunParallelPlugin: def __init__(self, config): self.verbose = bool(int(os.environ.get("PYTEST_RUN_PARALLEL_VERBOSE", "0"))) self.skip_thread_unsafe = config.option.skip_thread_unsafe self.mark_warnings_as_unsafe = config.option.mark_warnings_as_unsafe self.mark_ctypes_as_unsafe = config.option.mark_ctypes_as_unsafe self.mark_hypothesis_as_unsafe = config.option.mark_hypothesis_as_unsafe self.ignore_gil_enabled = config.option.ignore_gil_enabled if get_configured_num_workers(config) == 1: self.ignore_gil_enabled = 1 self.forever = config.option.forever skipped_functions = [ x.split(".") for x in config.getini("thread_unsafe_functions") ] self.skipped_functions = frozenset( (".".join(x[:-1]), x[-1]) for x in skipped_functions ) self.unsafe_fixtures = construct_thread_unsafe_fixtures(config) self.thread_unsafe = {} self.run_in_parallel = {} def skipped_or_not_parallel(self, *, plural): if plural: skipped = "were skipped" parallel = "were not run in parallel" else: skipped = "was skipped" parallel = "was not run in parallel" return skipped if self.skip_thread_unsafe else parallel def _mark_test_thread_unsafe(self, item, reason): if self.skip_thread_unsafe: item.add_marker(pytest.mark.skip(reason=reason)) else: item.add_marker(pytest.mark.parallel_threads(1)) self.thread_unsafe[item.nodeid] = reason def _is_thread_unsafe(self, item): m = item.get_closest_marker("thread_unsafe") if m is not None: reason = m.kwargs.get("reason", None) if reason is None: reason = "uses the thread_unsafe marker" return True, reason fixtures = getattr(item, "fixturenames", ()) if any(fixture in fixtures for fixture in self.unsafe_fixtures): used_unsafe_fixtures = self.unsafe_fixtures & set(fixtures) return True, f"uses thread-unsafe fixture(s): {used_unsafe_fixtures}" return identify_thread_unsafe_nodes( item.obj, self.skipped_functions, self.mark_warnings_as_unsafe, self.mark_ctypes_as_unsafe, self.mark_hypothesis_as_unsafe, ) @pytest.hookimpl(tryfirst=True) def pytest_runtestloop(self, session: pytest.Session): """ Based on the default implementation in pytest, but also adds support for running the tests in an endless loop. """ if not self.forever: # let the default pytest_runtestloop run if we don't need any # customization for --forever. return None if ( session.testsfailed and not session.config.option.continue_on_collection_errors ): raise session.Interrupted( "%d errors during collection" % session.testsfailed ) if session.config.option.collectonly: return True number_of_items = len(session.items) if number_of_items == 0: raise pytest.UsageError( "Test collection found zero tests when passing --forever. Are you sure you searched for the correct tests?" ) iter_number = 0 idx = 0 next_idx = (idx + 1) % number_of_items while idx < number_of_items: if idx == 0: print("\n\n", end="") print("==========================================================") print("You ran the test suite with 'forever' mode enabled.") print(f"Running the tests again. This is iteration #{iter_number}.") print("==========================================================") iter_number += 1 item = session.items[idx] nextitem = session.items[next_idx] if next_idx < number_of_items else None item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) if session.shouldfail: raise session.Failed(session.shouldfail) if session.shouldstop: raise session.Interrupted(session.shouldstop) idx = next_idx next_idx = (idx + 1) % number_of_items return True # This is tryfirst=True because we need our plugin's pytest_collection_finish # to be called before the terminal plugin's pytest_collection_finish: # # - pytest's terminal plugin hooks pytest_collection_finish, which calls # pytest_report_collectionfinish # - we hook pytest_report_collectionfinish, assuming _handle_collected_item # has already been called for all items when pytest_report_collectionfinish # is called @pytest.hookimpl(tryfirst=True) def pytest_collection_finish(self, session): for item in session.items: self._handle_collected_item(item) def _handle_collected_item(self, item): if not hasattr(item, "obj"): if not hasattr(item, "_parallel_custom_item"): warnings.warn( f"Encountered pytest item with type {type(item)} with no 'obj' " "attribute, which is incompatible with pytest-run-parallel. " f"Tests using {type(item)} will not run in a thread pool.\n" "The pytest-run-parallel plugin only supports custom collection " "tree objects that wrap Python functions stored in an attribute " "named 'obj'.\n" "Define a '_parallel_custom_item' attribute on the pytest item" "instance or class to silence this warning.\n" "If you do not want to use pytest-run-parallel, uninstall it from " "your environment." ) self._mark_test_thread_unsafe( item, "is incompatible with pytest-run-parallel" ) return if isinstance(item, _pytest.doctest.DoctestItem): self._mark_test_thread_unsafe( item, "is a doctest (pytest-run-parallel does not support doctests)" ) return n_workers, parallel_threads_marker_used = get_num_workers(item) if n_workers < 0: raise ValueError("parallel-threads cannot be negative") n_iterations, _ = get_num_iterations(item) if n_iterations < 0: raise ValueError("parallel-threads cannot be negative") if n_workers == 1 and parallel_threads_marker_used: self._mark_test_thread_unsafe(item, "test is marked as single-threaded") if n_workers > 1: thread_unsafe, reason = self._is_thread_unsafe(item) if thread_unsafe: n_workers = 1 self._mark_test_thread_unsafe(item, reason) else: self.run_in_parallel[item.nodeid] = n_workers if n_workers > 1 or n_iterations > 1: original_globals = item.obj.__globals__ item.obj = wrap_function_parallel(item.obj, n_workers, n_iterations) for name in original_globals: if name not in item.obj.__globals__: item.obj.__globals__[name] = original_globals[name] @pytest.hookimpl(trylast=True) def pytest_report_collectionfinish(self, config, start_path, items): return f"Collected {len(self.run_in_parallel)} items to run in parallel" @pytest.hookimpl(tryfirst=True, wrapper=True) def pytest_report_teststatus(self, report, config): outcome = yield if getattr(report, "when", None) != "call": return outcome if report.nodeid in self.run_in_parallel: if report.outcome == "passed": return "passed", "·", "PARALLEL PASSED" if report.outcome == "failed": return "error", "e", "PARALLEL FAILED" else: reason = self.thread_unsafe.get(report.nodeid) if reason is not None: if report.outcome == "passed": return ( "passed", ".", f"PASSED [thread-unsafe]: {reason}", ) if report.outcome == "failed": return ( "failed", "x", f"FAILED ([thread-unsafe]: {reason})", ) return outcome def _write_reasons_summary(self, terminalreporter): for nodeid, reason in self.thread_unsafe.items(): if reason is not None: terminalreporter.line( f"{nodeid} {self.skipped_or_not_parallel(plural=False)} because it {reason}" ) else: terminalreporter.line(nodeid) @pytest.hookimpl(trylast=True) def pytest_terminal_summary(self, terminalreporter, exitstatus, config): enabled = get_configured_num_workers(config) > 1 if not enabled: return terminalreporter.section("pytest-run-parallel report", "*") if self.verbose and self.thread_unsafe: self._write_reasons_summary(terminalreporter) elif self.thread_unsafe: num = len(self.thread_unsafe) if num > 1: test = "tests" else: test = "test" terminalreporter.line( f"{num} {test} {self.skipped_or_not_parallel(plural=num > 1)}" " because of use of thread-unsafe functionality, " f"to list the tests that {self.skipped_or_not_parallel(plural=True)}, re-run " "while setting PYTEST_RUN_PARALLEL_VERBOSE=1 " "in your shell environment" ) else: terminalreporter.line("All tests were run in parallel! 🎉") @pytest.hookimpl(tryfirst=True) def pytest_warning_recorded( self, warning_message: warnings.WarningMessage, when, nodeid, location ): mo = re.match(GIL_WARNING_MESSAGE_CONTENT, str(warning_message.message)) if mo is None or self.ignore_gil_enabled: return if when == "collect": stage = "collection" elif when == "runtest": stage = "execution" else: stage = "configuration" stage_test = stage if nodeid: stage_test += f" of '{nodeid}'" pytest.exit( reason=GIL_ENABLED_ERROR_TEXT.format( stage_test=stage_test, module=mo.group("module") ), returncode=1, ) @pytest.fixture def num_parallel_threads(request): return get_num_workers(request.node)[0] @pytest.fixture def num_iterations(request): return get_num_iterations(request.node)[0] # overwritten by wrap_function_parallel when using multiple threads @pytest.fixture def thread_index(): return 0 # overwritten by wrap_function_parallel when using multiple iterations @pytest.fixture def iteration_index(): return 0 @pytest.fixture def thread_comp(num_parallel_threads): return ThreadComparator(num_parallel_threads) def pytest_configure(config): if ( config.option.forever and (n := getattr(config.option, "numprocesses", None)) is not None ): raise pytest.UsageError( f"--forever from pytest-run-parallel is incompatible with `-n {n}` from pytest-xdist." ) config.addinivalue_line( "markers", "parallel_threads(n): run the given test function in parallel " "using `n` threads. Note that if n is greater than 1, the test " "run with this many threads even if the --parallel-threads " "command-line argument is not passed. Use parallel_threads_limit " "instead if you want to avoid this pitfall.", ) config.addinivalue_line( "markers", "parallel_threads_limit(n): run the given test function in parallel " "using a maximum of `n` threads.", ) config.addinivalue_line( "markers", "iterations(n): run the given test function `n` times in each thread", ) config.addinivalue_line( "markers", "thread_unsafe: mark the test function as single-threaded", ) config.pluginmanager.register(RunParallelPlugin(config), "_run-parallel") def pytest_addoption(parser): # Note: new options should be on group, not parser group = parser.getgroup("run-parallel") group.addoption( "--parallel-threads", action="store", dest="parallel_threads", default=1, help="Set the number of threads used to execute each test concurrently. (default: " "%(default)s)", ) group.addoption( "--iterations", action="store", dest="iterations", default=1, type=int, help="Set the number of iterations that each thread will run. (default: %(default)s)", ) group.addoption( "--skip-thread-unsafe", action="store", dest="skip_thread_unsafe", help="Whether to skip running thread-unsafe tests. If not provided, thread-unsafe tests " "will still run, but only in one thread.", type=bool, default=False, ) group.addoption( "--mark-warnings-as-unsafe", action="store_true", dest="mark_warnings_as_unsafe", default=False, help="Mark warnings capture, such as pytest.warns(), as thread-unsafe. If not provided, " "the thread safety of warnings capture will be determined automatically.", ) group.addoption( "--mark-ctypes-as-unsafe", action="store_true", dest="mark_ctypes_as_unsafe", default=False, help="Mark all uses of ctypes as thread-unsafe. If not provided, the thread safety of " "ctypes (but not the underlying C code) will be determined automatically.", ) group.addoption( "--mark-hypothesis-as-unsafe", action="store_true", dest="mark_hypothesis_as_unsafe", default=False, help="Mark hypothesis as thread-unsafe. If not provided, the thread safety of hypothesis " "will be determined automatically.", ) group.addoption( "--ignore-gil-enabled", action="store_true", dest="ignore_gil_enabled", default=False, help="Ignore the GIL becoming enabled in the middle of a test. By default, if the GIL is " "re-enabled at runtime, pytest will exit with a non-zero exit code. This option has no " "effect for non-free-threaded builds.", ) group.addoption( "--forever", action="store_true", dest="forever", default=False, help="Run the test loop forever (starting from the top when all the tests have been run), " "until one crashes or the user explicitly stops the process with Ctrl-C. This is especially " "helpful for hitting thread safety bugs that only occur rarely. --forever is not compatible " "with -n from pytest-xdist.", ) parser.addini( "thread_unsafe_fixtures", "list of thread-unsafe fixture names that cause a test to be run sequentially", type="linelist", default=[], ) parser.addini( "thread_unsafe_functions", "list of thread-unsafe fully-qualified named functions that cause " "a test to run on one thread", type="linelist", default=[], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel/thread_comparator.py0000644000175100017510000000515415131663774026230 0ustar00runnerrunnerimport threading import types try: import numpy as np numpy_available = True except ImportError: numpy_available = False class ThreadComparator: def __init__(self, n_threads): self._barrier = threading.Barrier(n_threads) self._reset_evt = threading.Event() self._entry_barrier = threading.Barrier(n_threads) self._thread_ids = [] self._values = {} self._entry_lock = threading.Lock() self._entry_counter = 0 def __call__(self, **values): """ Compares a set of values across threads. For each value, type equality as well as comparison takes place. If any of the values is a function, then address comparison is performed. Also, if any of the values is a `numpy.ndarray`, then approximate numerical comparison is performed. """ tid = id(threading.current_thread()) self._entry_barrier.wait() with self._entry_lock: if self._entry_counter == 0: # Reset state before comparison self._barrier.reset() self._reset_evt.clear() self._thread_ids = [] self._values = {} self._entry_barrier.reset() self._entry_counter += 1 self._values[tid] = values self._thread_ids.append(tid) self._barrier.wait() if tid == self._thread_ids[0]: thread_ids = list(self._values) try: for value_name in values: for i in range(1, len(thread_ids)): tid_a = thread_ids[i - 1] tid_b = thread_ids[i] value_a = self._values[tid_a][value_name] value_b = self._values[tid_b][value_name] assert type(value_a) is type(value_b) if numpy_available and isinstance(value_a, np.ndarray): if len(value_a.shape) == 0: assert value_a == value_b else: assert np.allclose(value_a, value_b, equal_nan=True) elif isinstance(value_a, types.FunctionType): assert id(value_a) == id(value_b) elif value_a != value_a: assert value_b != value_b else: assert value_a == value_b finally: self._entry_counter = 0 self._reset_evt.set() else: self._reset_evt.wait() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel/thread_unsafe_detection.py0000644000175100017510000003040015131663774027370 0ustar00runnerrunnerimport ast import functools import inspect import sys import traceback import warnings try: # added in hypothesis 6.131.0 from hypothesis import is_hypothesis_test except ImportError: try: # hypothesis versions < 6.131.0 from hypothesis.internal.detection import is_hypothesis_test except ImportError: # hypothesis isn't installed def is_hypothesis_test(fn): return False try: from hypothesis import __version_info__ as hypothesis_version except ImportError: hypothesis_version = (0, 0, 0) HYPOTHESIS_THREADSAFE_VERSION = (6, 136, 3) WARNINGS_IS_THREADSAFE = bool( getattr(sys.flags, "context_aware_warnings", 0) and getattr(sys.flags, "thread_inherit_context", 0) ) CTYPES_IS_THREADSAFE = sys.version_info > (3, 13) def construct_base_blocklist(unsafe_warnings, unsafe_ctypes): safe_warnings = not unsafe_warnings and WARNINGS_IS_THREADSAFE safe_ctypes = not unsafe_ctypes and CTYPES_IS_THREADSAFE return { ("pytest", "warns", safe_warnings), ("pytest", "deprecated_call", safe_warnings), ("_pytest.recwarn", "warns", safe_warnings), ("_pytest.recwarn", "deprecated_call", safe_warnings), ("warnings", "catch_warnings", safe_warnings), ("unittest.mock", "*", False), ("mock", "*", False), ("ctypes", "*", safe_ctypes), ("gc", "collect", False), } THREAD_UNSAFE_FIXTURES = { "capsys": False, "capteesys": False, "capsysbinary": False, "capfd": False, "capfdbinary": False, "monkeypatch": False, "recwarn": WARNINGS_IS_THREADSAFE, } class ThreadUnsafeNodeVisitor(ast.NodeVisitor): def __init__( self, fn, skip_set, unsafe_warnings, unsafe_ctypes, unsafe_hypothesis, level=0 ): self.thread_unsafe = False self.thread_unsafe_reason = None blocklist = construct_base_blocklist(unsafe_warnings, unsafe_ctypes) self.blocklist = {b[:2] for b in blocklist if not b[-1]} | skip_set self.module_blocklist = {mod for mod, func in self.blocklist if func == "*"} self.function_blocklist = { (mod, func) for mod, func in self.blocklist if func != "*" } modules = {mod.split(".")[0] for mod, _ in self.blocklist} modules |= {mod for mod, _ in self.blocklist} self.fn = fn self.skip_set = skip_set self.unsafe_warnings = unsafe_warnings self.unsafe_ctypes = unsafe_ctypes self.unsafe_hypothesis = unsafe_hypothesis self.level = level self.modules_aliases = {} self.func_aliases = {} self.globals = getattr(fn, "__globals__", {}) # see issue #121, sometimes __globals__ isn't iterable try: iter(self.globals) except TypeError: self.globals = {} for var_name in iter(self.globals): value = fn.__globals__[var_name] if inspect.ismodule(value) and value.__name__ in modules: self.modules_aliases[var_name] = value.__name__ elif inspect.isfunction(value): if value.__module__ is None: continue if value.__module__ in modules: self.func_aliases[var_name] = (value.__module__, value.__name__) continue all_parents = self._create_all_parent_modules(value.__module__) for parent in all_parents: if parent in modules: self.func_aliases[var_name] = (parent, value.__name__) break super().__init__() def _create_all_parent_modules(self, module_name): all_parent_modules = set() parent, dot, _ = module_name.rpartition(".") while dot: all_parent_modules.add(parent) parent, dot, _ = parent.rpartition(".") return all_parent_modules def _is_module_blocklisted(self, module_name): # fast path if module_name in self.module_blocklist: return True # try parent modules all_parents = self._create_all_parent_modules(module_name) if any(parent in self.module_blocklist for parent in all_parents): return True return False def _is_function_blocklisted(self, module_name, func_name): # Whole module is blocked if self._is_module_blocklisted(module_name): return True # Function is blocked if (module_name, func_name) in self.function_blocklist: return True return False def _recursive_analyze_attribute(self, node): current = node while isinstance(current.value, ast.Attribute): current = current.value if not isinstance(current.value, ast.Name): return id = current.value.id def _get_child_fn(mod, node): if isinstance(node.value, ast.Attribute): submod = _get_child_fn(mod, node.value) return getattr(submod, node.attr, None) if not isinstance(node.value, ast.Name): return None return getattr(mod, node.attr, None) if id in self.globals: mod = self.fn.__globals__[id] child_fn = _get_child_fn(mod, node) if child_fn is not None and callable(child_fn): self.thread_unsafe, self.thread_unsafe_reason = ( identify_thread_unsafe_nodes( child_fn, self.skip_set, self.unsafe_warnings, self.unsafe_ctypes, self.unsafe_hypothesis, self.level + 1, ) ) def _build_attribute_chain(self, node): chain = [] current = node while isinstance(current, ast.Attribute): chain.insert(0, current.attr) current = current.value if isinstance(current, ast.Name): chain.insert(0, current.id) return chain def _visit_attribute_call(self, node): if isinstance(node.value, ast.Name): real_mod = node.value.id if real_mod in self.modules_aliases: real_mod = self.modules_aliases[real_mod] if self._is_function_blocklisted(real_mod, node.attr): self.thread_unsafe = True self.thread_unsafe_reason = ( f"calls thread-unsafe function: {real_mod}.{node.attr}" ) elif self.level < 2: self._recursive_analyze_attribute(node) elif isinstance(node.value, ast.Attribute): chain = self._build_attribute_chain(node) module_part = ".".join(chain[:-1]) func_part = chain[-1] if self._is_function_blocklisted(module_part, func_part): self.thread_unsafe = True self.thread_unsafe_reason = ( f"calls thread-unsafe function: {'.'.join(chain)}" ) elif self.level < 2: self._recursive_analyze_attribute(node) def _recursive_analyze_name(self, node): if node.id in self.globals: child_fn = self.fn.__globals__[node.id] if callable(child_fn): self.thread_unsafe, self.thread_unsafe_reason = ( identify_thread_unsafe_nodes( child_fn, self.skip_set, self.unsafe_warnings, self.unsafe_ctypes, self.unsafe_hypothesis, self.level + 1, ) ) def _visit_name_call(self, node): if node.id in self.func_aliases: if self._is_function_blocklisted(*self.func_aliases[node.id]): self.thread_unsafe = True self.thread_unsafe_reason = f"calls thread-unsafe function: {node.id}" return if self.level < 2: self._recursive_analyze_name(node) def visit_Call(self, node): if isinstance(node.func, ast.Attribute): self._visit_attribute_call(node.func) elif isinstance(node.func, ast.Name): self._visit_name_call(node.func) self.generic_visit(node) def visit_Assign(self, node): if len(node.targets) == 1: name_node = node.targets[0] value_node = node.value if getattr(name_node, "id", None) == "__thread_safe__" and not bool( value_node.value ): self.thread_unsafe = True self.thread_unsafe_reason = ( f"calls thread-unsafe function: {self.fn.__name__} " "(inferred via func.__thread_safe__ == False)" ) return self.generic_visit(node) def visit(self, node): if self.thread_unsafe: return return super().visit(node) def _is_source_indented(src): # Find first nonblank line. If one can't be found, use placeholder. non_blank_lines = (line for line in src.split("\n") if line.strip() != "") first_non_blank_line = next(non_blank_lines, "pass") is_indented = first_non_blank_line[0].isspace() return is_indented def _visit_node(visitor, fn): try: src = inspect.getsource(fn) except (OSError, TypeError): # if we can't get the source code (e.g. builtin function) then give up # and don't attempt detection but default to assuming thread safety return False, None if _is_source_indented(src): # This test was extracted from a class or indented area, and Python # needs to be told to expect indentation. src = "if True:\n" + src try: tree = ast.parse(src) except (SyntaxError, ValueError): # AST parsing failed because the AST is invalid. Who knows why but that # means we can't run thread safety detection. Bail and assume # thread-safe. return False, None visitor.visit(tree) def _identify_thread_unsafe_nodes( fn, skip_set, unsafe_warnings, unsafe_ctypes, unsafe_hypothesis, level=0 ): if is_hypothesis_test(fn): if hypothesis_version < HYPOTHESIS_THREADSAFE_VERSION: return ( True, f"uses hypothesis v{'.'.join(map(str, hypothesis_version))}, which " "is before the first thread-safe version " f"(v{'.'.join(map(str, HYPOTHESIS_THREADSAFE_VERSION))})", ) if unsafe_hypothesis: return ( True, "uses Hypothesis, and pytest-run-parallel was run with " "--mark-hypothesis-as-unsafe", ) try: visitor = ThreadUnsafeNodeVisitor( fn, skip_set, unsafe_warnings, unsafe_ctypes, unsafe_hypothesis, level=level ) with warnings.catch_warnings(): # in case pytest is configured to treat warnings as errors. warnings.simplefilter("default") _visit_node(visitor, fn) except Exception as e: tb = traceback.format_exc() msg = ( f"Uncaught exception while checking test '{fn}' for thread-unsafe " "functionality. Please report a bug to pytest-run-parallel at " "https://github.com/Quansight-Labs/pytest-run-parallel/issues/new " "including this message if thread safety detection should work.\n" f"{e}\n{tb}\n" "Assuming this test is thread-safe." ) warnings.warn(msg, RuntimeWarning) return False, None return visitor.thread_unsafe, visitor.thread_unsafe_reason cached_thread_unsafe_identify = functools.lru_cache(_identify_thread_unsafe_nodes) def identify_thread_unsafe_nodes(*args, **kwargs): try: return cached_thread_unsafe_identify(*args, **kwargs) except TypeError: return _identify_thread_unsafe_nodes(*args, **kwargs) def construct_thread_unsafe_fixtures(config): unsafe_fixtures = THREAD_UNSAFE_FIXTURES.copy() for item in config.getini("thread_unsafe_fixtures"): unsafe_fixtures[item] = False if config.option.mark_warnings_as_unsafe: unsafe_fixtures["recwarn"] = False return {uf[0] for uf in unsafe_fixtures.items() if not uf[1]} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel/utils.py0000644000175100017510000000321115131663774023662 0ustar00runnerrunnerimport warnings from pytest_run_parallel.cpu_detection import get_logical_cpus def get_configured_num_workers(config): n_workers = config.option.parallel_threads if n_workers == "auto": logical_cpus = get_logical_cpus() n_workers = logical_cpus if logical_cpus is not None else 1 else: n_workers = int(n_workers) return n_workers def auto_or_int(val): if val == "auto": logical_cpus = get_logical_cpus() return logical_cpus if logical_cpus is not None else 1 return int(val) def get_num_workers(item): n_workers = get_configured_num_workers(item.config) # TODO: deprecate in favor of parallel_threads_limit marker_used = False marker = item.get_closest_marker("parallel_threads") if marker is not None: marker_used = True n_workers = auto_or_int(marker.args[0]) if n_workers > 1: warnings.warn( "Using the parallel_threads marker with a value greater than 1 is deprecated. Use parallel_threads_limit instead.", DeprecationWarning, stacklevel=2, ) limit_marker = item.get_closest_marker("parallel_threads_limit") if limit_marker is not None: val = auto_or_int(limit_marker.args[0]) if val == 1: marker_used = True if n_workers > val: n_workers = val return n_workers, marker_used def get_num_iterations(item): n_iterations = item.config.option.iterations marker = item.get_closest_marker("iterations") if marker is not None: n_iterations = int(marker.args[0]) return n_iterations, marker is not None ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1768384515.889737 pytest_run_parallel-0.8.2/src/pytest_run_parallel.egg-info/0000755000175100017510000000000015131664004023630 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384515.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel.egg-info/PKG-INFO0000644000175100017510000004057715131664003024741 0ustar00runnerrunnerMetadata-Version: 2.4 Name: pytest-run-parallel Version: 0.8.2 Summary: A simple pytest plugin to run tests concurrently Author: Edgar Margffoy Maintainer-email: Lysandros Nikolaou , Nathan Goldbaum License-Expression: MIT Project-URL: Repository, https://github.com/Quansight-Labs/pytest-run-parallel Classifier: Framework :: Pytest Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Testing Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=3.9 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: pytest>=6.2.0 Provides-Extra: psutil Requires-Dist: psutil>=6.1.1; extra == "psutil" Dynamic: license-file # pytest-run-parallel [![PyPI version](https://img.shields.io/pypi/v/pytest-run-parallel.svg)](https://pypi.org/project/pytest-run-parallel) [![Python versions](https://img.shields.io/pypi/pyversions/pytest-run-parallel.svg)](https://pypi.org/project/pytest-run-parallel) [![See Build Status on GitHub Actions](https://github.com/Quansight-Labs/pytest-run-parallel/actions/workflows/main.yml/badge.svg)](https://github.com/Quansight-Labs/pytest-run-parallel/actions/workflows/main.yml) A simple pytest plugin to run tests concurrently ------------------------------------------------------------------------ This [pytest](https://github.com/pytest-dev/pytest) plugin takes a set of tests that would be normally be run serially and execute them in parallel. The main goal of `pytest-run-parallel` is to discover thread-safety issues that could exist when using C libraries, this is of vital importance after [PEP703](https://peps.python.org/pep-0703/), which provides a path for a CPython implementation without depending on the Global Interpreter Lock (GIL), thus allowing for proper parallelism in programs that make use of the CPython interpreter. For more information about C thread-safety issues, please visit the free-threaded community guide at ## How it works This plugin is *not* an alternative to [pytest-xdist](https://pytest-xdist.readthedocs.io/) and does not run all of the tests in a test suite simultaneously in a thread pool. Instead, it runs many instances of the same test in a thread pool. It is only useful as a tool to do multithreaded stress tests using an existing test suite and is not useful to speed up the execution of a test suite via multithreaded parallelism. Given an existing test taking arguments `*args` and keyword arguments `**kwargs`, this plugin creates a new test that is equivalent to the following Python code: ```python import threading from concurrent.futures import ThreadPoolExecutor def run_test(b, *args, **kwargs): for _ in range(num_iterations): b.wait() execute_pytest_test(*args, **kwargs) with ThreadPoolExecutor(max_workers=num_parallel_threads) as tpe: b = threading.Barrier(num_parallel_threads) for _ in range(num_parallel_threads): tpe.submit(run_test, b, *args, **kwargs) ``` The `execute_pytest_test` function hides some magic to ensure errors and failures get propagated correctly to the main testing thread. Using this plugin avoids the boilerplate of rewriting existing tests to run in parallel in a thread pool. Note that `args` and `kwargs` might include pytest marks and fixtures, and the way this plugin is currently written, those fixtures are shared between threads. ## Features - Global CLI flags: - `--parallel-threads` to run a test suite in parallel - `--iterations` to run multiple times in each thread - `--skip-thread-unsafe` to skip running tests marked as or detected to be thread-unsafe. - `--mark-warnings-as-unsafe` and `--mark-ctypes-as-unsafe` to always skip running tests that use the `warnings` or `ctypes` modules, respectively. These are useful if you are adding support for Python 3.14 to a library that already runs tests under pytest-run-parallel on Python 3.13 or older. - `--mark-hypothesis-as-unsafe` to always skip running tests that use [hypothesis](https://github.com/hypothesisworks/hypothesis). While newer version of Hypothesis are thread-safe, and versions which are not are automatically skipped by `pytest-run-parallel`, this flag is an escape hatch in case you run into thread-safety problems caused by Hypothesis, or in tests that happen to use hypothesis and were skipped in older versions of pytest-run-parallel. - `--forever` to keep running tests in an endless loop starting from the top when completing a test run, until a test crashes or the user explicitly ends the process with Ctrl-C. This is especially helpful when trying to reproduce thread safety bugs that might only occur rarely. Note that pytest's progress indicator will keep showing 100% forever after the first pass of the test suite. `forever` is not compatible with `-n` from [pytest-xdist](https://github.com/pytest-dev/pytest-xdist). - `--ignore-gil-enabled` to ignore the RuntimeWarning generated when the GIL is enabled at runtime on the free-threaded build and run the tests despite the fact that the GIL is enabled. This option has no effect if pytest is configured to treat warnings as errors. - Four corresponding markers: - `pytest.mark.parallel_threads_limit(n)` to mark a single test to run in a maximum of `n` threads, even if the `--parallel-threads` command-line argument is set to a higher value. This is useful if a test uses resources that should be limited. - `pytest.mark.parallel_threads(n)` to mark a test to always run in `n` threads. Note that this implies that the test will be multi-threaded just because pytest-run-parallel is installed, even if `--parallel-threads` is not passed at the command-line. - `pytest.mark.thread_unsafe` to mark a single test to run in a single thread. It is equivalent to using `pytest.mark.parallel_threads(1)` - `pytest.mark.iterations(n)` to mark a single test to run `n` times in each thread - Four corresponding fixtures: - `num_parallel_threads`: The number of threads the test will run in - `num_iterations`: The number of iterations the test will run in each thread - `thread_index`: An index for the test's current thread. - `iteration_index`: An index for the test's current iteration. - Modifications to existing fixtures: - `tmp_path` and `tmpdir`: Patched to be thread-safe, with individual subdirectories being created for each thread. Subdirectories are not created for each iteration, so you may see issues with reused temporary directions when using `--iterations`. While pytest-run-parallel has special handling for the `tmp_path` and `tmpdir` fixtures to ensure that each thread has a private temporary directory, the plugin only does this if a test requests these fixtures directly. If another fixture requests `tmp_path` or `tmpdir`, then all threads will share a temporary directory in that fixture. When using the fixtures `thread_index` and `iteration_index`, they should be requested directly by tests, and will return 0 when requested by other fixtures. **Note**: It's possible to specify `--parallel-threads=auto` or `pytest.mark.parallel_threads("auto")` which will let `pytest-run-parallel` choose the number of logical CPU cores available to the testing process. If that cannot be determined, the number of physical CPU cores will be used. If that fails as well, it will fall back to running all tests single-threaded. ## Requirements `pytest-run-parallel` depends exclusively on `pytest`. Optionally installing `psutil` will help with identifying the number of logical cores available to the testing process in systems where that's not possible with the Python stdlib. ## Installation You can install "pytest-run-parallel" via [pip](https://pypi.org/project/pip/) from [PyPI](https://pypi.org/project): $ pip install pytest-run-parallel If you want to additionally install `psutil` you can run: $ pip install pytest-run-parallel[psutil] ## Caveats Pytest itself is not thread-safe and it is not safe to share stateful pytest fixtures or marks between threads. Existing tests relying on setting up mutable state via a fixture will see the state shared between threads. Tests that dynamically set marks or share marks will also likely not be thread-safe. See the pytest documentation [for more detail](https://docs.pytest.org/en/stable/explanation/flaky.html#thread-safety) and the community-maintained [free threaded Python porting guide](https://py-free-threading.github.io/porting/#pytest-is-not-thread-safe) for more detail about using pytest in a multithreaded context on the free-threaded build of Python. We suggest marking tests that are incompatible with this plugin's current design with `@pytest.mark.thread_unsafe` or `@pytest.mark.thread_unsafe(reason="...")`. The following functions and modules are known to be thread-unsafe and pytest-run-parallel will automatically skip running tests using them in parallel: - The pytest `capsys`, `capteesys`, `capsysbinary`, `capfd` and `capfdbinary` fixtures - The pytest `monkeypath` fixture The following fixtures are known to be thread-unsafe on Python 3.13 and older, or on 3.14 and newer if Python isn't configured correctly: - `pytest.warns` - `pytest.deprecated_call` - The pytest `recwarn` fixture - `warnings.catch_warnings` - `unittest.mock` - `ctypes` If an older version of `hypothesis` that is known to be thread-unsafe is installed, tests using `hypothesis` are skipped. Additionally, if a set of fixtures is known to be thread unsafe, tests that use them can be automatically marked as thread unsafe by declaring them under the thread_unsafe_fixtures option under pytest INI configuration file: ```ini [pytest] thread_unsafe_fixtures = fixture_1 fixture_2 ... ``` Or under the section `tool.pytest.ini_options` if using `pyproject.toml`: ```toml [tool.pytest.ini_options] thread_unsafe_fixtures = [ 'fixture_1', 'fixture_2', ... ] ``` Similarly, if a function is known to be thread unsafe and should cause a test to be marked as thread-unsafe as well, its fully-qualified name can be registered through the `thread_unsafe_functions` option in the INI file (or under `tool.pytest.ini_options` when using `pyproject.toml`): ```ini [pytest] thread_unsafe_functions = module.submodule.func1 module.submodule2.func2 ... ``` You can also blocklist entire modules by using an asterisk: ```ini [pytest] thread_unsafe_functions = module1.* module2.submodule.* ... ``` Also, if you define a `__thread_safe__ = False` attribute on a function that is called by a test and is up to two levels below in the call stack, then pytest-run-parallel will automatically detect that a thread-unsafe function is being used and will mark the test as thread-unsafe. ## Usage This plugin has two modes of operation, one via the `--parallel-threads` and `--iterations` pytest CLI flags, which allows a whole test suite to be run in parallel: $ pytest --parallel-threads=10 --iterations=10 tests By default, the value for both flags will be 1, thus not modifying the usual behaviour of pytest except when the flag is set. Note that using `pytest-xdist` and setting `iterations` to a number greater than one might cause tests to run even more times than intended. The other mode of operation occurs at the individual test level, via the `pytest.mark.parallel_threads` and `pytest.mark.iterations` markers: ```python # test_file.py import pytest @pytest.fixture def my_fixture(): ... @pytest.mark.parallel_threads(2) @pytest.mark.iterations(10) def test_something_1(): # This test will be run in parallel using two concurrent threads # and 10 times in each thread ... @pytest.mark.parametrize('arg', [1, 2, 3]) @pytest.mark.parallel_threads(3) def test_fixture(my_fixture, arg): # pytest markers and fixtures are supported as well ... ``` Both modes of operations are supported simultaneously, i.e., ```bash # test_something_1 and test_fixture will be run using their set number of # threads; other tests will be run using 5 threads. $ pytest -x -v --parallel-threads=5 test_file.py ``` You can skip tests marked as or detected to be thread-unsafe by passing `--skip-thread-unsafe` in your pytest invocation. This is useful when running pytest-run-parallel under [Thread Sanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html). Setting `--skip-thread-unsafe=True` will avoid unnecessarily running tests where thread sanitizer cannot detect races because the test is not parallelized. Older versions of pytest-run-parallel always marked tests using the `warnings` and `ctypes` modules as thread-unsafe, since both were not thread-safe until Python 3.14. If you are adding support for Python 3.14 and would like to continue marking tests that use `warnings` or `ctypes`, pass `--mark-warnings-as-unsafe` or `--mark-ctypes-as-unsafe`, respectively, in your `pytest` invocation. Additionally, `pytest-run-parallel` exposes the `num_parallel_threads` and `num_iterations` fixtures which enable a test to be aware of the number of threads that are being spawned and the number of iterations each test will run: ```python # test_file.py import pytest def test_skip_if_parallel(num_parallel_threads): if num_parallel_threads > 1: pytest.skip(reason='does not work in parallel') ... ``` The `thread_index` and `iteration_index` fixtures are also available, which enable tests to display different behavior between threads and iterations. ```python # test_file.py import numpy as np def test_unique_rng_streams(thread_index): # create an RNG stream with a seed that is deterministic # but still unique to this thread rng = np.random.default_rng(thread_index) ... ``` Finally, the `thread_comp` fixture allows for parallel test debugging, by providing an instance of `ThreadComparator`, whose `__call__` method allows to check if all the values produced by all threads during an specific execution step are the same: ``` python # test_file.py def test_same_execution_values(thread_comp): a = 2 b = [3, 4, 5] c = None # Check that the values for a, b, c are the same across tests thread_comp(a=a, b=b, c=c) ``` ## Tracing If you run pytest with verbose output (e.g. by passing `-v` in your pytest invocation), you will see that tests are annotated to either "PASS" or "PARALLEL PASS". A "PASS" indicates the test was run on a single thread, whereas "PARALLEL PASS" indicates the test passed and was run in a thread pool. If a test was not run in a thread pool because pytest-run-parallel detected use of thread-unsafe functionality, the reason will be printed as well. If you are running pytest in the default configuration without `-v`, then tests that pass in a thread pool will be annotated with a slightly different dot character, allowing you to visually pick out when tests are not run in parallel. For example in the output for this file: ``` tests/test_kx.py ·....· ``` Only the first and last tests are run in parallel. In order to list the tests that were marked as thread-unsafe and were not executed in parallel, you can set the `PYTEST_RUN_PARALLEL_VERBOSE` environment variable to 1. ## Contributing Contributions are very welcome. Tests can be run with `uv run pytest`. If you want to run `pytest` with coverage enabled you can run `uv run coverage run` and then `uv run coverage report -m` to get the coverage report. Please ensure the coverage at least stays the same before you submit a pull request. ## License Distributed under the terms of the [MIT](https://opensource.org/licenses/MIT) license, "pytest-run-parallel" is free and open source software ## Issues If you encounter any problems, please [file an issue](https://github.com/Quansight-Labs/pytest-run-parallel/issues) along with a detailed description. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384515.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel.egg-info/SOURCES.txt0000644000175100017510000000144415131664003025516 0ustar00runnerrunner.pre-commit-config.yaml LICENSE MANIFEST.in README.md RELEASE.md pyproject.toml uv.lock src/pytest_run_parallel/__init__.py src/pytest_run_parallel/cpu_detection.py src/pytest_run_parallel/plugin.py src/pytest_run_parallel/thread_comparator.py src/pytest_run_parallel/thread_unsafe_detection.py src/pytest_run_parallel/utils.py src/pytest_run_parallel.egg-info/PKG-INFO src/pytest_run_parallel.egg-info/SOURCES.txt src/pytest_run_parallel.egg-info/dependency_links.txt src/pytest_run_parallel.egg-info/entry_points.txt src/pytest_run_parallel.egg-info/requires.txt src/pytest_run_parallel.egg-info/top_level.txt tests/conftest.py tests/test_cpu_detection.py tests/test_run_parallel.py tests/test_thread_comparator.py tests/test_thread_index.py tests/test_thread_unsafe_detection.py tests/test_tmp_path.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384515.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel.egg-info/dependency_links.txt0000644000175100017510000000000115131664003027675 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384515.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel.egg-info/entry_points.txt0000644000175100017510000000006515131664003027126 0ustar00runnerrunner[pytest11] run-parallel = pytest_run_parallel.plugin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384515.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel.egg-info/requires.txt0000644000175100017510000000004615131664003026227 0ustar00runnerrunnerpytest>=6.2.0 [psutil] psutil>=6.1.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384515.0 pytest_run_parallel-0.8.2/src/pytest_run_parallel.egg-info/top_level.txt0000644000175100017510000000002415131664003026355 0ustar00runnerrunnerpytest_run_parallel ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1768384515.888737 pytest_run_parallel-0.8.2/tests/0000755000175100017510000000000015131664004016421 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/tests/conftest.py0000644000175100017510000000003415131663774020632 0ustar00runnerrunnerpytest_plugins = "pytester" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/tests/test_cpu_detection.py0000644000175100017510000001102115131663774022667 0ustar00runnerrunnerfrom contextlib import suppress import pytest try: import psutil except ImportError: psutil = None try: from os import process_cpu_count except ImportError: process_cpu_count = None try: from os import sched_getaffinity except ImportError: sched_getaffinity = None @pytest.mark.skipif(psutil is None, reason="psutil needs to be installed") def test_auto_detect_cpus_psutil_affinity( pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch ) -> None: import psutil monkeypatch.setattr( psutil.Process, "cpu_affinity", lambda self: list(range(10)), raising=False ) pytester.makepyfile(""" def test_auto_detect_cpus(num_parallel_threads): assert num_parallel_threads == 10 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=auto", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_auto_detect_cpus PARALLEL PASSED*", ] ) @pytest.mark.skipif(psutil is None, reason="psutil needs to be installed") def test_auto_detect_cpus_psutil_cpu_count( pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch ) -> None: import psutil monkeypatch.delattr(psutil.Process, "cpu_affinity", raising=False) monkeypatch.setattr(psutil, "cpu_count", lambda: 10) pytester.makepyfile(""" def test_auto_detect_cpus(num_parallel_threads): assert num_parallel_threads == 10 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=auto", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_auto_detect_cpus PARALLEL PASSED*", ] ) @pytest.mark.skipif( process_cpu_count is None, reason="process_cpu_count is available in >=3.13" ) def test_auto_detect_process_cpu_count( pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch ) -> None: with suppress(ImportError): import psutil monkeypatch.delattr(psutil.Process, "cpu_affinity", raising=False) monkeypatch.setattr(psutil, "cpu_count", lambda: None) monkeypatch.setattr("os.process_cpu_count", lambda: 10) pytester.makepyfile(""" def test_auto_detect_cpus(num_parallel_threads): assert num_parallel_threads == 10 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=auto", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_auto_detect_cpus PARALLEL PASSED*", ] ) @pytest.mark.skipif( sched_getaffinity is None, reason="sched_getaffinity is available certain platforms only", ) def test_auto_detect_sched_getaffinity( pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch ) -> None: with suppress(ImportError): import psutil monkeypatch.delattr(psutil.Process, "cpu_affinity", raising=False) monkeypatch.setattr(psutil, "cpu_count", lambda: None) monkeypatch.setattr("os.process_cpu_count", lambda: None, raising=False) monkeypatch.setattr("os.sched_getaffinity", lambda pid: list(range(10))) pytester.makepyfile(""" def test_auto_detect_cpus(num_parallel_threads): assert num_parallel_threads == 10 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=auto", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_auto_detect_cpus PARALLEL PASSED*", ] ) def test_auto_detect_cpu_count( pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch ) -> None: with suppress(ImportError): import psutil monkeypatch.delattr(psutil.Process, "cpu_affinity", raising=False) monkeypatch.setattr(psutil, "cpu_count", lambda: None) monkeypatch.setattr("os.process_cpu_count", lambda: None, raising=False) monkeypatch.setattr("os.sched_getaffinity", lambda pid: None, raising=False) monkeypatch.setattr("os.cpu_count", lambda: 10) pytester.makepyfile(""" def test_auto_detect_cpus(num_parallel_threads): assert num_parallel_threads == 10 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=auto", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_auto_detect_cpus PARALLEL PASSED*", ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/tests/test_run_parallel.py0000644000175100017510000006001515131663774022531 0ustar00runnerrunnerimport os import pytest try: import hypothesis except ImportError: hypothesis = None def test_default_threads(pytester): """Make sure that pytest accepts our fixture.""" # create a temporary pytest test module pytester.makepyfile(""" import pytest from threading import Lock class Counter: def __init__(self): self._count = 0 self._lock = Lock() def increase(self): with self._lock: self._count += 1 @pytest.fixture(scope='session') def counter(): return Counter() @pytest.mark.order(1) def test_thread_increase(counter): counter.increase() @pytest.mark.order(2) @pytest.mark.parallel_threads(1) def test_check_thread_count(counter): assert counter._count == 10 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_check_thread_count PASSED*", ] ) # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 def test_marker(pytester): # create a temporary pytest test module pytester.makepyfile(""" import pytest from threading import Lock class Counter: def __init__(self): self._count = 0 self._lock = Lock() def increase(self): with self._lock: self._count += 1 @pytest.fixture(scope='session') def counter(): return Counter() @pytest.fixture(scope='session') def counter2(): return Counter() @pytest.mark.order(1) def test_thread_increase(counter): counter.increase() @pytest.mark.order(1) @pytest.mark.parallel_threads(5) def test_thread_increase_five(counter2): counter2.increase() @pytest.mark.order(2) @pytest.mark.parallel_threads(1) def test_check_thread_count(counter): assert counter._count == 10 @pytest.mark.order(2) @pytest.mark.parallel_threads(1) def test_check_thread_count2(counter2): assert counter2._count == 5 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_check_thread_count PASSED*", "*::test_check_thread_count2 PASSED*", ] ) # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 def test_unittest_compat(pytester): # create a temporary pytest test module pytester.makepyfile(""" import pytest import unittest from threading import Lock class Counter: def __init__(self): self._count = 0 self._lock = Lock() def increase(self): with self._lock: self._count += 1 class TestExample(unittest.TestCase): @classmethod def setUpClass(cls): cls.counter = Counter() cls.counter2 = Counter() @pytest.mark.order(1) def test_example_1(self): self.counter.increase() @pytest.mark.order(1) @pytest.mark.parallel_threads(5) def test_example_2(self): self.counter2.increase() @pytest.mark.order(2) @pytest.mark.parallel_threads(1) def test_check_thread_count(self): assert self.counter._count == 10 @pytest.mark.order(2) @pytest.mark.parallel_threads(1) def test_check_thread_count2(self): assert self.counter2._count == 5 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_check_thread_count PASSED*", "*::test_check_thread_count2 PASSED*", ] ) # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 def test_help_message(pytester): result = pytester.runpytest( "--help", ) # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "run-parallel:", " --parallel-threads=PARALLEL_THREADS", " --iterations=ITERATIONS", ] ) def test_skip(pytester): """Make sure that pytest accepts our fixture.""" # create a temporary pytest test module pytester.makepyfile(""" import pytest def test_skipped(): pytest.skip('Skip propagation') """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_skipped SKIPPED*", ] ) # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 def test_fail(pytester): """Make sure that pytest accepts our fixture.""" # create a temporary pytest test module pytester.makepyfile(""" import pytest def test_should_fail(): pytest.fail() """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_should_fail PARALLEL FAILED*", ] ) # make sure that we get a '0' exit code for the testsuite assert result.ret != 0 def test_exception(pytester): """Make sure that pytest accepts our fixture.""" # create a temporary pytest test module pytester.makepyfile(""" import pytest def test_should_fail(): raise ValueError('Should raise') """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_should_fail PARALLEL FAILED*", ] ) # make sure that we get a '0' exit code for the testsuite assert result.ret != 0 def test_num_parallel_threads_fixture(pytester): """Test that the num_parallel_threads fixture works as expected.""" # create a temporary pytest test module pytester.makepyfile(""" import pytest def test_should_yield_global_threads(num_parallel_threads): assert num_parallel_threads == 10 @pytest.mark.parallel_threads(2) def test_should_yield_marker_threads(num_parallel_threads): assert num_parallel_threads == 2 @pytest.mark.parallel_threads(1) def test_single_threaded(num_parallel_threads): assert num_parallel_threads == 1 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_should_yield_global_threads PARALLEL PASSED*", "*::test_should_yield_marker_threads PARALLEL PASSED*", "*::test_single_threaded PASSED*", "*1 test was not run in parallel because of use of " "thread-unsafe functionality, to list the tests that " "were not run in parallel, re-run while setting PYTEST_RUN_PARALLEL_VERBOSE=1" " in your shell environment", ] ) # Re-run with verbose output orig = os.environ.get("PYTEST_RUN_PARALLEL_VERBOSE", "0") os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = "1" result = pytester.runpytest("--parallel-threads=10", "-v") os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = orig result.stdout.fnmatch_lines( ["*pytest-run-parallel report*", "*::test_single_threaded*"], consecutive=True, ) def test_parallel_threads_limit_fixture(pytester): """Test that the num_parallel_threads fixture works as expected.""" # create a temporary pytest test module pytester.makepyfile(""" import pytest def test_should_yield_global_threads(num_parallel_threads): assert num_parallel_threads == 10 @pytest.mark.parallel_threads_limit(20) def test_unaffected_by_thread_limit(num_parallel_threads): assert num_parallel_threads == 10 @pytest.mark.parallel_threads_limit(5) def test_less_than_thread_limit(num_parallel_threads): assert num_parallel_threads == 5 @pytest.mark.parallel_threads_limit(1) def test_single_threaded(num_parallel_threads): assert num_parallel_threads == 1 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_unaffected_by_thread_limit PARALLEL PASSED*", "*::test_less_than_thread_limit PARALLEL PASSED*", "*::test_single_threaded PASSED*", "*1 test was not run in parallel because of use of " "thread-unsafe functionality, to list the tests that " "were not run in parallel, re-run while setting PYTEST_RUN_PARALLEL_VERBOSE=1" " in your shell environment", ] ) # Re-run with verbose output orig = os.environ.get("PYTEST_RUN_PARALLEL_VERBOSE", "0") os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = "1" result = pytester.runpytest("--parallel-threads=10", "-v") os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = orig result.stdout.fnmatch_lines( ["*pytest-run-parallel report*", "*::test_single_threaded*"], consecutive=True, ) def test_parallel_threads_limit_one_thread(pytester): """Test that the num_parallel_threads fixture works as expected.""" # create a temporary pytest test module pytester.makepyfile(""" import pytest def test_should_yield_global_threads(num_parallel_threads): assert num_parallel_threads == 1 @pytest.mark.parallel_threads_limit(5) def test_marker_threads_five(num_parallel_threads): assert num_parallel_threads == 1 @pytest.mark.parallel_threads_limit(2) def test_marker_threads_two(num_parallel_threads): assert num_parallel_threads == 1 @pytest.mark.parallel_threads_limit(1) def test_marker_threads_one(num_parallel_threads): assert num_parallel_threads == 1 """) # run pytest with the following cmd args result = pytester.runpytest("-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_marker_threads_five PASSED*[[]???%[]]", "*::test_marker_threads_two PASSED*[[]???%[]]", "*::test_marker_threads_one PASSED*[[]thread-unsafe[]]*", ] ) def test_iterations_marker_one_thread(pytester): # create a temporary pytest test module pytester.makepyfile(""" import pytest from threading import Lock class Counter: def __init__(self): self._count = 0 self._lock = Lock() def increase(self): with self._lock: self._count += 1 @pytest.fixture(scope='session') def counter(): return Counter() @pytest.mark.order(1) @pytest.mark.parallel_threads(1) @pytest.mark.iterations(10) def test_thread_increase(counter): counter.increase() @pytest.mark.order(2) @pytest.mark.parallel_threads(1) @pytest.mark.iterations(1) def test_check_thread_count(counter): assert counter._count == 10 """) # run pytest with the following cmd args result = pytester.runpytest("-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_check_thread_count PASSED*", ] ) # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 def test_iterations_config_one_thread(pytester): # create a temporary pytest test module pytester.makepyfile(""" import pytest from threading import Lock class Counter: def __init__(self): self._count = 0 self._lock = Lock() def increase(self): with self._lock: self._count += 1 @pytest.fixture(scope='session') def counter(): return Counter() @pytest.mark.order(1) @pytest.mark.parallel_threads(1) def test_thread_increase(counter): counter.increase() @pytest.mark.order(2) @pytest.mark.parallel_threads(1) @pytest.mark.iterations(1) def test_check_thread_count(counter): assert counter._count == 10 """) # run pytest with the following cmd args result = pytester.runpytest("--iterations=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_check_thread_count PASSED*", ] ) # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 def test_multiple_iterations_multiple_threads(pytester): # create a temporary pytest test module pytester.makepyfile(""" import pytest from threading import Lock class Counter: def __init__(self): self._count = 0 self._lock = Lock() def increase(self): with self._lock: self._count += 1 @pytest.fixture(scope='session') def counter(): return Counter() @pytest.mark.order(1) @pytest.mark.parallel_threads(10) @pytest.mark.iterations(10) def test_thread_increase(counter): counter.increase() @pytest.mark.order(2) @pytest.mark.parallel_threads(1) @pytest.mark.iterations(1) def test_check_thread_count(counter): assert counter._count == 10 * 10 """) # run pytest with the following cmd args result = pytester.runpytest("-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_check_thread_count PASSED*", ] ) # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 def test_num_iterations_fixture(pytester): """Test that the num_iterations fixture works as expected.""" # create a temporary pytest test module pytester.makepyfile(""" import pytest def test_should_yield_global_threads(num_iterations): assert num_iterations == 10 @pytest.mark.iterations(2) def test_should_yield_marker_threads(num_iterations): assert num_iterations == 2 """) # run pytest with the following cmd args result = pytester.runpytest("--iterations=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_should_yield_global_threads PASSED*", "*::test_should_yield_marker_threads PASSED*", ] ) def test_skipif_marker_works(pytester): # create a temporary pytest test module pytester.makepyfile(""" import pytest VAR = 1 @pytest.mark.skipif('VAR == 1', reason='VAR is 1') def test_should_skip(): pass """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_should_skip SKIPPED*", ] ) def test_incompatible_test_item(pytester): pytester.makeconftest(""" import inspect import pytest class CustomItem(pytest.Item): def __init__(self, name, parent=None, config=None, session=None, nodeid=None, function=None, **kwargs): super().__init__(name, parent, config, session, nodeid, **kwargs) self.function = function def runtest(self): self.function() @pytest.hookimpl(wrapper=True, trylast=True) def pytest_pycollect_makeitem(collector, name: str, obj: object): result = yield if not inspect.isfunction(obj): return result return CustomItem.from_parent(name=name, parent=collector, function=obj) """) pytester.makepyfile(""" import pytest def test_incompatible_item(): assert True """) result = pytester.runpytest("--parallel-threads=10", "-v", "-W", "default") result.stdout.fnmatch_lines( [ "*::test_incompatible_item PASSED*", ] ) assert result.parseoutcomes()["warnings"] == 1 def test_known_incompatible_test_item_doesnt_warn(pytester): pytester.makeconftest(""" import inspect import pytest class CustomItem(pytest.Item): def __init__(self, name, parent=None, config=None, session=None, nodeid=None, function=None, **kwargs): super().__init__(name, parent, config, session, nodeid, **kwargs) self.function = function self._parallel_custom_item = True def runtest(self): self.function() @pytest.hookimpl(wrapper=True, trylast=True) def pytest_pycollect_makeitem(collector, name: str, obj: object): result = yield if not inspect.isfunction(obj): return result return CustomItem.from_parent(name=name, parent=collector, function=obj) """) pytester.makepyfile(""" import pytest def test_incompatible_item(): assert True """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*::test_incompatible_item PASSED*", ] ) result.stderr.no_fnmatch_line( "*Encountered pytest item with type " "with no 'obj'*" ) assert "warnings" not in result.parseoutcomes().keys() def test_all_tests_in_parallel(pytester): pytester.makepyfile(""" def test_parallel_1(num_parallel_threads): assert num_parallel_threads == 10 def test_parallel_2(num_parallel_threads): assert num_parallel_threads == 10 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*All tests were run in parallel! 🎉*", ] ) # re-run with PYTEST_RUN_PARALLEL_VERBOSE=1 orig = os.environ.get("PYTEST_RUN_PARALLEL_VERBOSE", "0") os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = "1" result = pytester.runpytest("--parallel-threads=10", "-v") os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = orig result.stdout.fnmatch_lines( [ "*All tests were run in parallel! 🎉*", ] ) def test_doctests_marked_thread_unsafe(pytester): pytester.makepyfile(""" def test_parallel(num_parallel_threads): assert num_parallel_threads == 10 """) pytester.makefile( ".txt", """ hello this is a doctest >>> x = 3 >>> x 3 >>> num_parallel_threads = getfixture("num_parallel_threads") >>> num_parallel_threads 1 """, ) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*::test_parallel PARALLEL PASSED*", "*::test_doctests_marked_thread_unsafe.txt PASSED*", ] ) @pytest.mark.skipif(hypothesis is None, reason="hypothesis needs to be installed") def test_runs_hypothesis_in_parallel(pytester): pytester.makepyfile(""" from hypothesis import given, strategies as st, settings, HealthCheck @given(a=st.none()) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) def test_uses_hypothesis(a, num_parallel_threads): assert num_parallel_threads == 10 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*::test_uses_hypothesis PARALLEL PASSED*", ] ) def test_fail_warning_gil_enabled_during_execution(pytester): test_name = "test_fail_warning_gil_enabled_during_execution" pytester.makepyfile(f""" import warnings def {test_name}(): warnings.warn( "The global interpreter lock (GIL) has been enabled to load module 'module'", RuntimeWarning ) """) result = pytester.runpytest("-v", "-W", "default") assert result.ret == 0 result.parseoutcomes()["warnings"] == 1 result = pytester.runpytest("-v", "--parallel-threads=2", "-W", "default") assert result.ret == 1 result.stdout.fnmatch_lines( [ f"*GIL was dynamically re-enabled during test execution of '{test_name}.py::{test_name}' to load module 'module'*" ] ) def test_fail_warning_gil_enabled_during_collection(pytester): test_name = "test_fail_warning_gil_enabled_during_collection" pytester.makepyfile(f""" import warnings warnings.warn( "The global interpreter lock (GIL) has been enabled to load module 'module'", RuntimeWarning ) def {test_name}(): assert True """) result = pytester.runpytest("-v", "-W", "default", "--parallel-threads=2") assert result.ret == 1 result.stdout.fnmatch_lines( [ "*GIL was dynamically re-enabled during test collection to load module 'module'*" ] ) def test_warning_gil_enabled_ignore_option(pytester): pytester.makepyfile(""" import warnings warnings.warn( "The global interpreter lock (GIL) has been enabled to load module 'module'", RuntimeWarning ) def test_warning_gil_enabled_ignore_option(): assert True """) result = pytester.runpytest( "-v", "--ignore-gil-enabled", "-W", "default", "--parallel-threads=2" ) assert result.ret == 0 def test_runs_with_xdist(pytester): pytester.makepyfile(""" def test_parallel1(num_parallel_threads): assert num_parallel_threads == 10 def test_parallel2(num_parallel_threads): assert num_parallel_threads == 10 """) result = pytester.runpytest("--parallel-threads=10", "-n", "2", "-v") result.stdout.fnmatch_lines(["*test_parallel1*"]) result.stdout.fnmatch_lines(["*test_parallel2*"]) result.stdout.fnmatch_lines(["*All tests were run in parallel!*"]) assert result.ret == 0 def test_forever_with_xdist_errors(pytester): pytester.makepyfile(""" def test_example(): assert True """) result = pytester.runpytest("--forever", "-n", "2") assert result.ret != 0 result.stderr.fnmatch_lines( [ "*--forever from pytest-run-parallel is incompatible with `-n 2` from pytest-xdist*" ] ) def test_parallel_threads_deprecation(pytester): pytester.makepyfile(""" import pytest @pytest.mark.parallel_threads(1) def test_parallel_threads_one(): assert True @pytest.mark.parallel_threads(2) def test_parallel_threads_two(): assert True """) result = pytester.runpytest("-v") assert result.ret == 0 result.stdout.fnmatch_lines( [ "*::test_parallel_threads_one PASSED*", "*::test_parallel_threads_two PARALLEL PASSED*", "*pytest_run_parallel/plugin.py:*DeprecationWarning: Using the parallel_threads marker*", ] ) def test_forever_without_selected_tests(pytester): pytester.makepyfile("") result = pytester.runpytest("--forever", "-v") assert result.ret == pytest.ExitCode.USAGE_ERROR result.stderr.fnmatch_lines( [ "*Test collection found zero tests when passing --forever. Are you sure you searched for the correct tests?*" ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/tests/test_thread_comparator.py0000644000175100017510000000313415131663774023546 0ustar00runnerrunnerdef test_thread_comp_fixture(pytester): """Test that ThreadComparator works as expected.""" # create a temporary pytest test module pytester.makepyfile(""" import threading import pytest class Counter: def __init__(self): self._value = 0 self._lock = threading.Lock() def get_value_and_increment(self): with self._lock: value = int(self._value) self._value += 1 return value def test_value_comparison(num_parallel_threads, thread_comp): assert num_parallel_threads == 10 a = 1 b = [2, 'string', 1.0] c = {'a': -4, 'b': 'str'} d = float('nan') e = float('inf') f = {'a', 'b', '#'} thread_comp(a=a, b=b, c=c, d=d, e=e, f=f) # Ensure that the comparator can be used again thread_comp(g=4) @pytest.fixture def counter(num_parallel_threads): return Counter() def test_comparison_fail(thread_comp, counter): a = 4 pos = counter.get_value_and_increment() if pos % 2 == 0: a = -1 thread_comp(a=a) """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_value_comparison PARALLEL PASSED*", "*::test_comparison_fail PARALLEL FAILED*", ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/tests/test_thread_index.py0000644000175100017510000000524515131663774022513 0ustar00runnerrunnerimport pytest def test_thread_index_single_thread(pytester: pytest.Pytester) -> None: pytester.makepyfile(""" def test_thread_index(thread_index): assert thread_index == 0 """) result = pytester.runpytest("--parallel-threads=1", "-v") result.stdout.fnmatch_lines( [ "*::test_thread_index PASSED*", ] ) def test_thread_index_num_parallel_threads(pytester: pytest.Pytester) -> None: pytester.makepyfile(""" def test_thread_index(thread_index, num_parallel_threads): assert thread_index < num_parallel_threads """) result = pytester.runpytest("--parallel-threads=auto", "-v") result.stdout.fnmatch_lines( [ "*::test_thread_index PARALLEL PASSED*", ] ) def test_thread_index_changes_between_tests(pytester: pytest.Pytester) -> None: # thread_comp is checking if the thread_indexes are equal between threads. # should fail since thread_indexes should not match. # test can be improved, since this cannot check if every thread has a # different thread_index pytester.makepyfile(""" def test_thread_index(thread_index, thread_comp): thread_comp(thread_index=thread_index) """) result = pytester.runpytest("--parallel-threads=auto", "-v") result.stdout.fnmatch_lines( [ "*::test_thread_index PARALLEL FAILED*", ] ) def test_iteration_index_single_iteration(pytester: pytest.Pytester) -> None: pytester.makepyfile(""" def test_iteration_index(iteration_index): assert iteration_index == 0 """) result = pytester.runpytest("--parallel-threads=auto", "--iterations=1", "-v") result.stdout.fnmatch_lines( [ "*::test_iteration_index PARALLEL PASSED*", ] ) def test_iteration_index_multi_iteration(pytester: pytest.Pytester) -> None: pytester.makepyfile(""" def test_iteration_index(iteration_index, num_iterations): assert iteration_index < num_iterations """) result = pytester.runpytest("--parallel-threads=1", "--iterations=3", "-v") result.stdout.fnmatch_lines( [ "*::test_iteration_index PASSED*", ] ) def test_iteration_index_multi_iteration_mutli_thread( pytester: pytest.Pytester, ) -> None: pytester.makepyfile(""" def test_iteration_index(iteration_index, num_iterations): assert iteration_index < num_iterations """) result = pytester.runpytest("--parallel-threads=auto", "--iterations=3", "-v") result.stdout.fnmatch_lines( [ "*::test_iteration_index PARALLEL PASSED*", ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/tests/test_thread_unsafe_detection.py0000644000175100017510000006005715131663774024725 0ustar00runnerrunnerimport os import sys import textwrap import pytest from pytest_run_parallel.thread_unsafe_detection import identify_thread_unsafe_nodes try: import hypothesis except ImportError: hypothesis = None WARNINGS_IS_THREADSAFE = bool( getattr(sys.flags, "context_aware_warnings", 0) and getattr(sys.flags, "thread_inherit_context", 0) ) WARNINGS_PASS = "PARALLEL " if WARNINGS_IS_THREADSAFE else "" CTYPES_PASS = "PARALLEL " if sys.version_info > (3, 13) else "" def test_thread_unsafe_marker(pytester): # create a temporary pytest test module pytester.makepyfile(""" import pytest @pytest.mark.thread_unsafe def test_should_run_single(num_parallel_threads): assert num_parallel_threads == 1 @pytest.mark.thread_unsafe(reason='this is thread-unsafe') def test_should_run_single_2(num_parallel_threads): assert num_parallel_threads == 1 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_should_run_single PASSED*", "*::test_should_run_single_2 PASSED *thread-unsafe*: this is thread-unsafe*", "*2 tests were not run in parallel*", ] ) # check that skipping works too result = pytester.runpytest( "--parallel-threads=10", "--skip-thread-unsafe=True", "-v" ) result.stdout.fnmatch_lines( [ "*::test_should_run_single SKIPPED*", "*::test_should_run_single_2 SKIPPED*", "*2 tests were skipped*", ] ) result.stdout.no_fnmatch_line("*All tests were run in parallel*") def test_pytest_warns_detection(pytester): # create a temporary pytest test module pytester.makepyfile(""" import pytest import warnings import pytest as pyt import warnings as w import sys from pytest import warns, deprecated_call from warnings import catch_warnings warns_alias = warns WARNINGS_IS_THREADSAFE = ( getattr(sys.flags, "context_aware_warnings", 0) and getattr(sys.flags, "thread_inherit_context", 0)) def test_single_thread_warns_1(num_parallel_threads): with pytest.warns(UserWarning): warnings.warn('example', UserWarning) if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 def test_single_thread_warns_2(num_parallel_threads): with warns(UserWarning): warnings.warn('example', UserWarning) if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 def test_single_thread_warns_3(num_parallel_threads): with pyt.warns(UserWarning): warnings.warn('example', UserWarning) if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 def test_single_thread_warns_4(num_parallel_threads): with warns_alias(UserWarning): warnings.warn('example', UserWarning) if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ f"*::test_single_thread_warns_1 {WARNINGS_PASS}PASSED*", f"*::test_single_thread_warns_2 {WARNINGS_PASS}PASSED*", f"*::test_single_thread_warns_3 {WARNINGS_PASS}PASSED*", f"*::test_single_thread_warns_4 {WARNINGS_PASS}PASSED*", ] ) if not ( getattr(sys.flags, "context_aware_warnings", 0) or getattr(sys.flags, "thread_inherit_context", 0) ): # check that skipping works too result = pytester.runpytest( "--parallel-threads=10", "--skip-thread-unsafe=True", "-v" ) result.stdout.fnmatch_lines( [ "*::test_single_thread_warns_1 SKIPPED*", "*::test_single_thread_warns_2 SKIPPED*", "*::test_single_thread_warns_3 SKIPPED*", "*::test_single_thread_warns_4 SKIPPED*", ] ) def test_warns_detection_config_option(pytester): # create a temporary pytest test module pytester.makepyfile(""" import pytest import warnings import pytest as pyt import warnings as w import sys from pytest import warns, deprecated_call from warnings import catch_warnings warns_alias = warns def test_single_thread_warns_1(num_parallel_threads): with pytest.warns(UserWarning): warnings.warn('example', UserWarning) assert num_parallel_threads == 1 def test_single_thread_warns_2(num_parallel_threads): with warns(UserWarning): warnings.warn('example', UserWarning) assert num_parallel_threads == 1 def test_single_thread_warns_3(num_parallel_threads): with pyt.warns(UserWarning): warnings.warn('example', UserWarning) assert num_parallel_threads == 1 def test_single_thread_warns_4(num_parallel_threads): with warns_alias(UserWarning): warnings.warn('example', UserWarning) assert num_parallel_threads == 1 """) # run pytest with the following cmd args result = pytester.runpytest( "--parallel-threads=10", "-v", "--mark-warnings-as-unsafe" ) # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_single_thread_warns_1 PASSED*", "*::test_single_thread_warns_2 PASSED*", "*::test_single_thread_warns_3 PASSED*", "*::test_single_thread_warns_4 PASSED*", ] ) # check that skipping works too result = pytester.runpytest( "--parallel-threads=10", "--skip-thread-unsafe=True", "-v", "--mark-warnings-as-unsafe", ) result.stdout.fnmatch_lines( [ "*::test_single_thread_warns_1 SKIPPED*", "*::test_single_thread_warns_2 SKIPPED*", "*::test_single_thread_warns_3 SKIPPED*", "*::test_single_thread_warns_4 SKIPPED*", ] ) def test_thread_unsafe_fixtures(pytester): # create a temporary pytest test module pytester.makepyfile(""" import sys import pytest @pytest.fixture def my_unsafe_fixture(): pass @pytest.fixture def my_unsafe_fixture_2(): pass WARNINGS_IS_THREADSAFE = ( getattr(sys.flags, "context_aware_warnings", 0) and getattr(sys.flags, "thread_inherit_context", 0)) def test_capsys(capsys, num_parallel_threads): assert num_parallel_threads == 1 def test_capteesys(capteesys, num_parallel_threads): assert num_parallel_threads == 1 def test_capsysbinary(capsysbinary, num_parallel_threads): assert num_parallel_threads == 1 def test_capfd(capfd, num_parallel_threads): assert num_parallel_threads == 1 def test_capfdbinary(capfdbinary, num_parallel_threads): assert num_parallel_threads == 1 def test_monkeypatch(monkeypatch, num_parallel_threads): assert num_parallel_threads == 1 def test_recwarn(recwarn, num_parallel_threads): if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 def test_custom_fixture_skip(my_unsafe_fixture, num_parallel_threads): assert num_parallel_threads == 1 def test_custom_fixture_skip_2(my_unsafe_fixture_2, num_parallel_threads): assert num_parallel_threads == 1 """) pytester.makeini(""" [pytest] thread_unsafe_fixtures = my_unsafe_fixture my_unsafe_fixture_2 """) # run pytest with the following cmd args result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*::test_capsys PASSED *thread-unsafe*: uses thread-unsafe fixture*", "*::test_capteesys PASSED *thread-unsafe*: uses thread-unsafe fixture*", "*::test_capsysbinary PASSED *thread-unsafe*: uses thread-unsafe fixture*", "*::test_capfd PASSED *thread-unsafe*: uses thread-unsafe fixture*", "*::test_capfdbinary PASSED *thread-unsafe*: uses thread-unsafe fixture*", f"*::test_recwarn {WARNINGS_PASS}PASSED*", "*::test_custom_fixture_skip PASSED *thread-unsafe*: uses thread-unsafe fixture*", "*::test_custom_fixture_skip_2 PASSED *thread-unsafe*: uses thread-unsafe fixture*", ] ) def test_thread_unsafe_function_attr(pytester): pytester.makepyfile( mod_1=""" def to_skip(): __thread_safe__ = False def not_to_skip(): __thread_safe__ = True """ ) pytester.makepyfile( mod_2=""" import mod_1 from mod_1 import not_to_skip def some_fn_calls_skip(): mod_1.to_skip() def some_fn_should_not_skip(): not_to_skip() def marked_for_skip(): pass """ ) pytester.makepyfile(""" import mod_2 from mod_2 import some_fn_calls_skip def test_should_be_marked_1(num_parallel_threads): mod_2.some_fn_calls_skip() assert num_parallel_threads == 1 def test_should_not_be_marked(num_parallel_threads): mod_2.some_fn_should_not_skip() assert num_parallel_threads == 10 def test_should_be_marked_2(num_parallel_threads): mod_2.marked_for_skip() assert num_parallel_threads == 1 def test_should_be_marked_3(num_parallel_threads): some_fn_calls_skip() assert num_parallel_threads == 1 """) pytester.makeini(""" [pytest] thread_unsafe_functions = mod_2.marked_for_skip """) # run pytest with the following cmd args orig = os.environ.get("PYTEST_RUN_PARALLEL_VERBOSE", "0") os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = "0" result = pytester.runpytest("--parallel-threads=10", "-v") # fnmatch_lines does an assertion internally result.stdout.fnmatch_lines( [ "*Collected 1 items to run in parallel*", "*::test_should_be_marked_1 PASSED *thread-unsafe*inferred via func.__thread_safe__*", "*::test_should_not_be_marked PARALLEL PASSED*", "*::test_should_be_marked_2 PASSED *thread-unsafe*marked_for_skip*", "*::test_should_be_marked_3 PASSED *thread-unsafe*inferred via func.__thread_safe__*", ] ) result.stdout.fnmatch_lines( [ "*3 tests were not run in parallel because of use of thread-unsafe " "functionality, to list the tests that were not run in parallel, " "re-run while setting PYTEST_RUN_PARALLEL_VERBOSE=1 in your " "shell environment*", ] ) # re-run with PYTEST_RUN_PARALLEL_VERBOSE=1 os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = "1" result = pytester.runpytest("--parallel-threads=10", "-v") os.environ["PYTEST_RUN_PARALLEL_VERBOSE"] = orig result.stdout.fnmatch_lines( [ "*Collected 1 items to run in parallel*", "*::test_should_be_marked_1 PASSED *thread-unsafe*: calls thread-unsafe function*", "*::test_should_not_be_marked PARALLEL PASSED*", "*::test_should_be_marked_2 PASSED*", "*::test_should_be_marked_3 PASSED*", "*::test_should_be_marked_1*", "*::test_should_be_marked_2*", "*::test_should_be_marked_3*", ] ) def test_detect_unittest_mock(pytester): pytester.makepyfile(""" import sys from unittest import mock @mock.patch("sys.platform", "VAX") def test_uses_mock(num_parallel_threads): assert sys.platform == "VAX" assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ r"*::test_uses_mock PASSED*" r"calls thread-unsafe function: mock.patch*", ] ) def test_recurse_assign(pytester): pytester.makepyfile(""" import pytest def unsafe(): __thread_safe__ = False def test_function_recurse_on_assign(num_parallel_threads): w = unsafe() assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*::test_function_recurse_on_assign PASSED*", ] ) def test_failed_thread_unsafe(pytester): pytester.makepyfile(""" import pytest @pytest.mark.thread_unsafe def test1(): assert False """) result = pytester.runpytest("--parallel-threads=10", "-v") assert result.ret == 1 print(result.stdout) result.stdout.fnmatch_lines( [ "*::test1 FAILED *thread-unsafe*: uses the thread_unsafe marker*", "* FAILURES *", "*1 failed*", ] ) def test_chained_attribute_import(pytester): pytester.makepyfile(""" import _pytest.recwarn import sys WARNINGS_IS_THREADSAFE = ( getattr(sys.flags, "context_aware_warnings", 0) and getattr(sys.flags, "thread_inherit_context", 0)) def test_chained_attribute_thread_unsafe_detection(num_parallel_threads): _pytest.recwarn.warns() if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ f"*::test_chained_attribute_thread_unsafe_detection {WARNINGS_PASS}PASSED*", ] ) def test_chained_attribute_thread_safe_assignment(pytester): pytester.mkpydir("mod") file = pytester.path / "mod" / "submod.py" file.write_text( textwrap.dedent(""" def to_skip(): __thread_safe__ = False """) ) pytester.makepyfile(""" import mod.submod def test_chained_attribute_thread_safe_assignment(num_parallel_threads): mod.submod.to_skip() assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*::test_chained_attribute_thread_safe_assignment PASSED*", ] ) def test_wrapped_function_call(pytester): pytester.makepyfile(""" def unsafe(x): __thread_safe__ = False return x def wrapper(x): return x def test_wrapped_function_call(num_parallel_threads): wrapper(unsafe(1)) assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*::test_wrapped_function_call PASSED*", ] ) def test_thread_unsafe_function_call_in_assignment(pytester): pytester.makepyfile(""" def unsafe(): __thread_safe__ = False return 1 def test_thread_unsafe_function_call_in_assignment(num_parallel_threads): x = y = unsafe() assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*::test_thread_unsafe_function_call_in_assignment PASSED*", ] ) def test_thread_unsafe_unittest_mock_patch_object(pytester): pytester.makepyfile(""" import sys import unittest.mock @unittest.mock.patch.object(sys, "platform", "VAX") def test_thread_unsafe_unittest_mock_patch_object(num_parallel_threads): assert sys.platform == "VAX" assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*::test_thread_unsafe_unittest_mock_patch_object PASSED*", ] ) def test_thread_unsafe_ctypes(pytester): pytester.makepyfile(""" import ctypes.util import sys CTYPES_IS_THREADSAFE = sys.version_info > (3, 13) def test_thread_unsafe_ctypes(num_parallel_threads): ctypes.util.find_library("m") if CTYPES_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ f"*::test_thread_unsafe_ctypes {CTYPES_PASS}PASSED*", ] ) def test_thread_unsafe_ctypes_config_option(pytester): pytester.makepyfile(""" import ctypes.util import sys def test_thread_unsafe_ctypes(num_parallel_threads): ctypes.util.find_library("m") assert num_parallel_threads == 1 """) result = pytester.runpytest( "--parallel-threads=10", "-v", "--mark-ctypes-as-unsafe" ) result.stdout.fnmatch_lines( [ "*::test_thread_unsafe_ctypes PASSED*", ] ) def test_thread_unsafe_ctypes_import_from(pytester): pytester.makepyfile(""" import sys from ctypes.util import find_library CTYPES_IS_THREADSAFE = sys.version_info > (3, 13) def test_thread_unsafe_ctypes(num_parallel_threads): find_library("m") if CTYPES_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 def test_thread_unsafe_not_using_ctypes(num_parallel_threads): assert num_parallel_threads == 10 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ f"*::test_thread_unsafe_ctypes {CTYPES_PASS}PASSED*", "*::test_thread_unsafe_not_using_ctypes PARALLEL PASSED*", ] ) def test_thread_unsafe_ctypes_import_from_config_option(pytester): pytester.makepyfile(""" import sys from ctypes.util import find_library def test_thread_unsafe_ctypes(num_parallel_threads): find_library("m") assert num_parallel_threads == 1 def test_thread_unsafe_not_using_ctypes(num_parallel_threads): assert num_parallel_threads == 10 """) result = pytester.runpytest( "--parallel-threads=10", "-v", "--mark-ctypes-as-unsafe" ) result.stdout.fnmatch_lines( [ "*::test_thread_unsafe_ctypes PASSED*", "*::test_thread_unsafe_not_using_ctypes PARALLEL PASSED*", ] ) def test_thread_unsafe_pytest_warns_multiline_string(pytester): pytester.makepyfile(""" import sys import warnings import pytest WARNINGS_IS_THREADSAFE = ( getattr(sys.flags, "context_aware_warnings", 0) and getattr(sys.flags, "thread_inherit_context", 0)) class TestThreadUnsafePytestWarnsMultilineString: def test_thread_unsafe_pytest_warns_multiline_string1(self, num_parallel_threads): with pytest.warns(UserWarning) as r: warnings.warn("foo", UserWarning) ''' Hello world''' if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 def test_thread_unsafe_pytest_warns_multiline_string2(self, num_parallel_threads): with pytest.warns(UserWarning) as r: warnings.warn("foo", UserWarning) ''' Hello world''' if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ f"*::test_thread_unsafe_pytest_warns_multiline_string1 {WARNINGS_PASS}PASSED*", f"*::test_thread_unsafe_pytest_warns_multiline_string2 {WARNINGS_PASS}PASSED*", ] ) def test_thread_unsafe_pytest_warns_instance_decorator(pytester): pytester.makepyfile(""" import sys import warnings import pytest WARNINGS_IS_THREADSAFE = ( getattr(sys.flags, "context_aware_warnings", 0) and getattr(sys.flags, "thread_inherit_context", 0)) def identity(func): # Decorator that does nothing return func class TestThreadUnsafePytestWarnsInstanceDecorator: @identity def test_thread_unsafe_pytest_warns_instance_decorator(self, num_parallel_threads): with pytest.warns(UserWarning) as r: warnings.warn("foo", UserWarning) if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 @identity def test_thread_unsafe_pytest_warns_instance_decorator_with_multiline(self, num_parallel_threads): with pytest.warns(UserWarning) as r: warnings.warn("foo", UserWarning) '''foo ''' if WARNINGS_IS_THREADSAFE: assert num_parallel_threads == 10 else: assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ f"*::test_thread_unsafe_pytest_warns_instance_decorator {WARNINGS_PASS}PASSED*", f"*::test_thread_unsafe_pytest_warns_instance_decorator_with_multiline {WARNINGS_PASS}PASSED*", ] ) @pytest.mark.skipif(hypothesis is None, reason="hypothesis needs to be installed") def test_thread_unsafe_hypothesis_config_option(pytester): pytester.makepyfile(""" from hypothesis import given, strategies as st, settings, HealthCheck @given(st.integers()) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) def test_thread_unsafe_hypothesis(num_parallel_threads, n): assert num_parallel_threads == 1 assert isinstance(n, int) """) result = pytester.runpytest( "--parallel-threads=10", "-v", "--mark-hypothesis-as-unsafe" ) result.stdout.fnmatch_lines( [ "*::test_thread_unsafe_hypothesis PASSED*", ] ) def test_thread_unsafe_detection_can_handle_none_module(pytester): pytester.makepyfile( """ global_lambda = eval("lambda x: x + 1", {}) def test_has_l_in_globals(num_parallel_threads): assert num_parallel_threads == 10 assert global_lambda.__module__ is None assert "global_lambda" in test_has_l_in_globals.__globals__ """ ) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ "*::test_has_l_in_globals PARALLEL PASSED*", ] ) def test_ast_parsing_error(): def dummy_test(): assert True class RaisesError: def __init__(self, wrapped_test): self.wrapped_test = wrapped_test def __getattr__(self, name): if name == "__globals__": raise RuntimeError("Intentionally break AST parsing") return self.wrapped_test.__getattribute__(name) def __call__(self, *args, **kwargs): return self.wrapped_test(*args, **kwargs) msg = ( r"Uncaught exception while checking test[\s\S]*" "Intentionally break AST parsing" ) with pytest.warns(RuntimeWarning, match=msg): identify_thread_unsafe_nodes( RaisesError(dummy_test), frozenset(), False, False, False ) def test_detect_gc_collect(pytester): pytester.makepyfile(""" import gc def test_gc_collect(num_parallel_threads): gc.collect() assert num_parallel_threads == 1 """) result = pytester.runpytest("--parallel-threads=10", "-v") result.stdout.fnmatch_lines( [ r"*::test_gc_collect PASSED*" r"calls thread-unsafe function: gc.collect*", ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/tests/test_tmp_path.py0000644000175100017510000001147415131663774021672 0ustar00runnerrunnerimport pytest parallel_threads = [ (1, "PASSED"), # no parallel threads ("auto", "PARALLEL PASSED"), # parallel threads ] @pytest.mark.parametrize("parallel, passing", parallel_threads) def test_tmp_path_is_empty(pytester: pytest.Pytester, parallel, passing): # ensures tmp_path is empty for each thread # test from (gh-109) pytester.makepyfile(""" def test_tmp_path(tmp_path): print(tmp_path) assert tmp_path.exists() assert tmp_path.is_dir() assert len(list(tmp_path.iterdir())) == 0 d = tmp_path / "sub" assert not d.exists() d.mkdir() assert d.exists() """) result = pytester.runpytest(f"--parallel-threads={parallel}", "-v") result.stdout.fnmatch_lines( [ f"*::test_tmp_path {passing}*", ] ) @pytest.mark.parametrize("parallel, passing", parallel_threads) def test_tmp_path_read_write(pytester: pytest.Pytester, parallel, passing): # ensures we can read/write in each tmp_path pytester.makepyfile(""" def test_tmp_path(tmp_path): file = tmp_path / "file" with open(file, "w") as f: f.write("Hello world!") assert file.is_file() assert file.read_text() == "Hello world!" """) result = pytester.runpytest(f"--parallel-threads={parallel}", "-v") result.stdout.fnmatch_lines( [ f"*::test_tmp_path {passing}*", ] ) @pytest.mark.parametrize("parallel, passing", parallel_threads) def test_tmp_path_delete(pytester: pytest.Pytester, parallel, passing): # ensures we can delete files in each tmp_path pytester.makepyfile(""" def test_tmp_path(tmp_path): subdir = tmp_path / "subdir" subdir.mkdir() file = subdir / "file" with open(file, "w") as f: f.write("Hello world!") assert file.is_file() file.unlink() assert not file.exists() subdir.rmdir() assert not subdir.exists() """) result = pytester.runpytest(f"--parallel-threads={parallel}", "-v") result.stdout.fnmatch_lines( [ f"*::test_tmp_path {passing}*", ] ) @pytest.mark.parametrize("parallel, passing", parallel_threads) def test_tmpdir_is_empty(pytester: pytest.Pytester, parallel, passing): # ensures tmpdir is empty for each thread pytester.makepyfile(""" def test_tmpdir(tmpdir): assert tmpdir.check() assert tmpdir.check(dir=1) assert len(list(tmpdir.listdir())) == 0 assert not tmpdir.join("sub").check() assert tmpdir.mkdir("sub").check() """) result = pytester.runpytest(f"--parallel-threads={parallel}", "-v") result.stdout.fnmatch_lines( [ f"*::test_tmpdir {passing}*", ] ) @pytest.mark.parametrize("parallel, passing", parallel_threads) def test_tmpdir_read_write(pytester: pytest.Pytester, parallel, passing): # ensures we can read/write in each tmpdir pytester.makepyfile(""" def test_tmpdir(tmpdir): file = tmpdir.join("file") with open(file, "w") as f: f.write("Hello world!") assert file.check(file=1) assert file.read_text("utf-8") == "Hello world!" """) result = pytester.runpytest(f"--parallel-threads={parallel}", "-v") result.stdout.fnmatch_lines( [ f"*::test_tmpdir {passing}*", ] ) @pytest.mark.parametrize("parallel, passing", parallel_threads) def test_tmpdir_delete(pytester: pytest.Pytester, parallel, passing): # ensures we can delete files in each tmpdir pytester.makepyfile(""" def test_tmpdir(tmpdir): subdir = tmpdir.mkdir("sub") file = tmpdir.join("file") with open(file, "w") as f: f.write("Hello world!") assert file.check(file=1) file.remove() assert not file.check() subdir.remove() assert not subdir.check() """) result = pytester.runpytest(f"--parallel-threads={parallel}", "-v") result.stdout.fnmatch_lines( [ f"*::test_tmpdir {passing}*", ] ) @pytest.mark.parametrize("parallel, passing", parallel_threads) def test_tmp_path_tmpdir(pytester: pytest.Pytester, parallel, passing): # ensures tmp_path and tmpdir can be used at the same time pytester.makepyfile(""" def test_both(tmp_path, tmpdir): assert tmp_path.exists() assert tmpdir.check(dir=1) assert tmp_path == tmpdir """) result = pytester.runpytest(f"--parallel-threads={parallel}", "-v") result.stdout.fnmatch_lines( [ f"*::test_both {passing}*", ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1768384508.0 pytest_run_parallel-0.8.2/uv.lock0000644000175100017510000032727415131663774016617 0ustar00runnerrunnerversion = 1 revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.10'", "python_full_version < '3.10'", ] [[package]] name = "attrs" version = "25.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] name = "build" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] [[package]] name = "check-manifest" version = "0.50" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "build" }, { name = "setuptools" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/ab/7607952f2c8d34c4124309dd3ea17c256fd3420a4ade01322daf9402b0b5/check_manifest-0.50.tar.gz", hash = "sha256:d300f9f292986aa1a30424af44eb45c5644e0a810e392e62d553b24bb3393494", size = 44827, upload-time = "2024-10-09T08:10:01.71Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/71/55/92207fa9b92ac2ade5593b1280f804f2590a680b7fe96775eb26074eec6b/check_manifest-0.50-py3-none-any.whl", hash = "sha256:6ab3e3aa72a008da3314b432f4c768c9647b4d6d8032f9e1a4672a572118e48c", size = 20385, upload-time = "2024-10-09T08:09:59.963Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.10.7" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version < '3.10'" }, ] [[package]] name = "coverage" version = "7.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.10'", ] sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/12/95/c49df0aceb5507a80b9fe5172d3d39bf23f05be40c23c8d77d556df96cec/coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31", size = 215800, upload-time = "2025-10-15T15:12:19.824Z" }, { url = "https://files.pythonhosted.org/packages/dc/c6/7bb46ce01ed634fff1d7bb53a54049f539971862cc388b304ff3c51b4f66/coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075", size = 216198, upload-time = "2025-10-15T15:12:22.549Z" }, { url = "https://files.pythonhosted.org/packages/94/b2/75d9d8fbf2900268aca5de29cd0a0fe671b0f69ef88be16767cc3c828b85/coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab", size = 242953, upload-time = "2025-10-15T15:12:24.139Z" }, { url = "https://files.pythonhosted.org/packages/65/ac/acaa984c18f440170525a8743eb4b6c960ace2dbad80dc22056a437fc3c6/coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0", size = 244766, upload-time = "2025-10-15T15:12:25.974Z" }, { url = "https://files.pythonhosted.org/packages/d8/0d/938d0bff76dfa4a6b228c3fc4b3e1c0e2ad4aa6200c141fcda2bd1170227/coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785", size = 246625, upload-time = "2025-10-15T15:12:27.387Z" }, { url = "https://files.pythonhosted.org/packages/38/54/8f5f5e84bfa268df98f46b2cb396b1009734cfb1e5d6adb663d284893b32/coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591", size = 243568, upload-time = "2025-10-15T15:12:28.799Z" }, { url = "https://files.pythonhosted.org/packages/68/30/8ba337c2877fe3f2e1af0ed7ff4be0c0c4aca44d6f4007040f3ca2255e99/coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088", size = 244665, upload-time = "2025-10-15T15:12:30.297Z" }, { url = "https://files.pythonhosted.org/packages/cc/fb/c6f1d6d9a665536b7dde2333346f0cc41dc6a60bd1ffc10cd5c33e7eb000/coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f", size = 242681, upload-time = "2025-10-15T15:12:32.326Z" }, { url = "https://files.pythonhosted.org/packages/be/38/1b532319af5f991fa153c20373291dc65c2bf532af7dbcffdeef745c8f79/coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866", size = 242912, upload-time = "2025-10-15T15:12:34.079Z" }, { url = "https://files.pythonhosted.org/packages/67/3d/f39331c60ef6050d2a861dc1b514fa78f85f792820b68e8c04196ad733d6/coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841", size = 243559, upload-time = "2025-10-15T15:12:35.809Z" }, { url = "https://files.pythonhosted.org/packages/4b/55/cb7c9df9d0495036ce582a8a2958d50c23cd73f84a23284bc23bd4711a6f/coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf", size = 218266, upload-time = "2025-10-15T15:12:37.429Z" }, { url = "https://files.pythonhosted.org/packages/68/a8/b79cb275fa7bd0208767f89d57a1b5f6ba830813875738599741b97c2e04/coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969", size = 219169, upload-time = "2025-10-15T15:12:39.25Z" }, { url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912, upload-time = "2025-10-15T15:12:40.665Z" }, { url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310, upload-time = "2025-10-15T15:12:42.461Z" }, { url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706, upload-time = "2025-10-15T15:12:44.001Z" }, { url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634, upload-time = "2025-10-15T15:12:45.768Z" }, { url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741, upload-time = "2025-10-15T15:12:47.222Z" }, { url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837, upload-time = "2025-10-15T15:12:48.904Z" }, { url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429, upload-time = "2025-10-15T15:12:50.73Z" }, { url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490, upload-time = "2025-10-15T15:12:52.646Z" }, { url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208, upload-time = "2025-10-15T15:12:54.586Z" }, { url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126, upload-time = "2025-10-15T15:12:56.485Z" }, { url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314, upload-time = "2025-10-15T15:12:58.365Z" }, { url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203, upload-time = "2025-10-15T15:12:59.902Z" }, { url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879, upload-time = "2025-10-15T15:13:01.35Z" }, { url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" }, { url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" }, { url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" }, { url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" }, { url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" }, { url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" }, { url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" }, { url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" }, { url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" }, { url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" }, { url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" }, { url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" }, { url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" }, { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, ] [[package]] name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, ] [[package]] name = "execnet" version = "2.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" }, ] [[package]] name = "filelock" version = "3.16.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037, upload-time = "2024-09-17T19:02:01.779Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163, upload-time = "2024-09-17T19:02:00.268Z" }, ] [[package]] name = "hypothesis" version = "6.140.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/7f/946343e32881b56adc0eba64e428ad2f85251f9ef16e3e4ec1b6ab80199b/hypothesis-6.140.3.tar.gz", hash = "sha256:4f4a09bf77af21e0cc3dffed1ea639812dc75d38f81308ec9fb0e33f8557b0cb", size = 466925, upload-time = "2025-10-04T22:29:44.499Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/65/2a/0553ac2a8af432df92f2ffc05ca97e7ed64e00c97a371b019ae2690de325/hypothesis-6.140.3-py3-none-any.whl", hash = "sha256:a2cfff51641a58a56081f5c90ae1da6ccf3d043404f411805f7f0e0d75742d0e", size = 534534, upload-time = "2025-10-04T22:29:40.635Z" }, ] [[package]] name = "identify" version = "2.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097, upload-time = "2024-09-14T23:50:32.513Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972, upload-time = "2024-09-14T23:50:30.747Z" }, ] [[package]] name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] name = "packaging" version = "24.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788, upload-time = "2024-06-09T23:19:24.956Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985, upload-time = "2024-06-09T23:19:21.909Z" }, ] [[package]] name = "platformdirs" version = "4.3.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] name = "pre-commit" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/b3/4ae08d21eb097162f5aad37f4585f8069a86402ed7f5362cc9ae097f9572/pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", size = 177079, upload-time = "2023-10-13T15:57:48.334Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6c/75/526915fedf462e05eeb1c75ceaf7e3f9cde7b5ce6f62740fe5f7f19a0050/pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660", size = 203698, upload-time = "2023-10-13T15:57:46.378Z" }, ] [[package]] name = "psutil" version = "6.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502, upload-time = "2024-12-19T18:21:20.568Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511, upload-time = "2024-12-19T18:21:45.163Z" }, { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985, upload-time = "2024-12-19T18:21:49.254Z" }, { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488, upload-time = "2024-12-19T18:21:51.638Z" }, { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477, upload-time = "2024-12-19T18:21:55.306Z" }, { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017, upload-time = "2024-12-19T18:21:57.875Z" }, { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602, upload-time = "2024-12-19T18:22:08.808Z" }, { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyproject-hooks" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] [[package]] name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] [[package]] name = "pytest-order" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/66/02ae17461b14a52ce5a29ae2900156b9110d1de34721ccc16ccd79419876/pytest_order-1.3.0.tar.gz", hash = "sha256:51608fec3d3ee9c0adaea94daa124a5c4c1d2bb99b00269f098f414307f23dde", size = 47544, upload-time = "2024-08-22T12:29:54.512Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/73/59b038d1aafca89f8e9936eaa8ffa6bb6138d00459d13a32ce070be4f280/pytest_order-1.3.0-py3-none-any.whl", hash = "sha256:2cd562a21380345dd8d5774aa5fd38b7849b6ee7397ca5f6999bbe6e89f07f6e", size = 14609, upload-time = "2024-08-22T12:29:53.156Z" }, ] [[package]] name = "pytest-run-parallel" version = "0.8.2" source = { editable = "." } dependencies = [ { name = "pytest" }, ] [package.optional-dependencies] psutil = [ { name = "psutil" }, ] [package.dev-dependencies] dev = [ { name = "check-manifest" }, { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, { name = "coverage", version = "7.11.0", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, { name = "hypothesis" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-order" }, { name = "pytest-xdist" }, { name = "ruff" }, ] test = [ { name = "check-manifest" }, { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, { name = "coverage", version = "7.11.0", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, { name = "hypothesis" }, { name = "pytest" }, { name = "pytest-order" }, { name = "pytest-xdist" }, ] [package.metadata] requires-dist = [ { name = "psutil", marker = "extra == 'psutil'", specifier = ">=6.1.1" }, { name = "pytest", specifier = ">=6.2.0" }, ] provides-extras = ["psutil"] [package.metadata.requires-dev] dev = [ { name = "check-manifest" }, { name = "coverage", extras = ["toml"], specifier = ">=7.8" }, { name = "hypothesis", specifier = ">=6.135.33" }, { name = "pre-commit", specifier = ">=3.5.0" }, { name = "pytest", specifier = ">=8.4.0" }, { name = "pytest-order" }, { name = "pytest-xdist" }, { name = "ruff", specifier = ">=0.7.2" }, ] test = [ { name = "check-manifest" }, { name = "coverage", extras = ["toml"], specifier = ">=7.8" }, { name = "hypothesis", specifier = ">=6.135.33" }, { name = "pytest", specifier = ">=8.4.0" }, { name = "pytest-order" }, { name = "pytest-xdist" }, ] [[package]] name = "pytest-xdist" version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] name = "ruff" version = "0.7.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/51/231bb3790e5b0b9fd4131f9a231d73d061b3667522e3f406fd9b63334d0e/ruff-0.7.2.tar.gz", hash = "sha256:2b14e77293380e475b4e3a7a368e14549288ed2931fce259a6f99978669e844f", size = 3210036, upload-time = "2024-11-01T15:07:27.364Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/56/0caa2b5745d66a39aa239c01059f6918fc76ed8380033d2f44bf297d141d/ruff-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:b73f873b5f52092e63ed540adefc3c36f1f803790ecf2590e1df8bf0a9f72cb8", size = 10373973, upload-time = "2024-11-01T15:06:42.937Z" }, { url = "https://files.pythonhosted.org/packages/1a/33/cad6ff306731f335d481c50caa155b69a286d5b388e87ff234cd2a4b3557/ruff-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5b813ef26db1015953daf476202585512afd6a6862a02cde63f3bafb53d0b2d4", size = 10171140, upload-time = "2024-11-01T15:06:45.871Z" }, { url = "https://files.pythonhosted.org/packages/97/f5/6a2ca5c9ba416226eac9cf8121a1baa6f06655431937e85f38ffcb9d0d01/ruff-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:853277dbd9675810c6826dad7a428d52a11760744508340e66bf46f8be9701d9", size = 9809333, upload-time = "2024-11-01T15:06:48.613Z" }, { url = "https://files.pythonhosted.org/packages/16/83/e3e87f13d1a1dc205713632978cd7bc287a59b08bc95780dbe359b9aefcb/ruff-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21aae53ab1490a52bf4e3bf520c10ce120987b047c494cacf4edad0ba0888da2", size = 10622987, upload-time = "2024-11-01T15:06:51.404Z" }, { url = "https://files.pythonhosted.org/packages/22/16/97ccab194480e99a2e3c77ae132b3eebfa38c2112747570c403a4a13ba3a/ruff-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc7e0fc6e0cb3168443eeadb6445285abaae75142ee22b2b72c27d790ab60ba", size = 10184640, upload-time = "2024-11-01T15:06:54.209Z" }, { url = "https://files.pythonhosted.org/packages/97/1b/82ff05441b036f68817296c14f24da47c591cb27acfda473ee571a5651ac/ruff-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd77877a4e43b3a98e5ef4715ba3862105e299af0c48942cc6d51ba3d97dc859", size = 11210203, upload-time = "2024-11-01T15:06:56.953Z" }, { url = "https://files.pythonhosted.org/packages/a6/96/7ecb30a7ef7f942e2d8e0287ad4c1957dddc6c5097af4978c27cfc334f97/ruff-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e00163fb897d35523c70d71a46fbaa43bf7bf9af0f4534c53ea5b96b2e03397b", size = 11870894, upload-time = "2024-11-01T15:06:59.679Z" }, { url = "https://files.pythonhosted.org/packages/06/6a/c716bb126218227f8e604a9c484836257708a05ee3d2ebceb666ff3d3867/ruff-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3c54b538633482dc342e9b634d91168fe8cc56b30a4b4f99287f4e339103e88", size = 11449533, upload-time = "2024-11-01T15:07:01.762Z" }, { url = "https://files.pythonhosted.org/packages/e6/2f/3a5f9f9478904e5ae9506ea699109070ead1e79aac041e872cbaad8a7458/ruff-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b792468e9804a204be221b14257566669d1db5c00d6bb335996e5cd7004ba80", size = 12607919, upload-time = "2024-11-01T15:07:04.546Z" }, { url = "https://files.pythonhosted.org/packages/a0/57/4642e57484d80d274750dcc872ea66655bbd7e66e986fede31e1865b463d/ruff-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dba53ed84ac19ae4bfb4ea4bf0172550a2285fa27fbb13e3746f04c80f7fa088", size = 11016915, upload-time = "2024-11-01T15:07:06.796Z" }, { url = "https://files.pythonhosted.org/packages/4d/6d/59be6680abee34c22296ae3f46b2a3b91662b8b18ab0bf388b5eb1355c97/ruff-0.7.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b19fafe261bf741bca2764c14cbb4ee1819b67adb63ebc2db6401dcd652e3748", size = 10625424, upload-time = "2024-11-01T15:07:09.553Z" }, { url = "https://files.pythonhosted.org/packages/82/e7/f6a643683354c9bc7879d2f228ee0324fea66d253de49273a0814fba1927/ruff-0.7.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28bd8220f4d8f79d590db9e2f6a0674f75ddbc3847277dd44ac1f8d30684b828", size = 10233692, upload-time = "2024-11-01T15:07:12.564Z" }, { url = "https://files.pythonhosted.org/packages/d7/48/b4e02fc835cd7ed1ee7318d9c53e48bcf6b66301f55925a7dcb920e45532/ruff-0.7.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9fd67094e77efbea932e62b5d2483006154794040abb3a5072e659096415ae1e", size = 10751825, upload-time = "2024-11-01T15:07:14.773Z" }, { url = "https://files.pythonhosted.org/packages/1e/06/6c5ee6ab7bb4cbad9e8bb9b2dd0d818c759c90c1c9e057c6ed70334b97f4/ruff-0.7.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:576305393998b7bd6c46018f8104ea3a9cb3fa7908c21d8580e3274a3b04b691", size = 11074811, upload-time = "2024-11-01T15:07:16.857Z" }, { url = "https://files.pythonhosted.org/packages/a1/16/8969304f25bcd0e4af1778342e63b715e91db8a2dbb51807acd858cba915/ruff-0.7.2-py3-none-win32.whl", hash = "sha256:fa993cfc9f0ff11187e82de874dfc3611df80852540331bc85c75809c93253a8", size = 8650268, upload-time = "2024-11-01T15:07:19.755Z" }, { url = "https://files.pythonhosted.org/packages/d9/18/c4b00d161def43fe5968e959039c8f6ce60dca762cec4a34e4e83a4210a0/ruff-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dd8800cbe0254e06b8fec585e97554047fb82c894973f7ff18558eee33d1cb88", size = 9433693, upload-time = "2024-11-01T15:07:22.513Z" }, { url = "https://files.pythonhosted.org/packages/7f/7b/c920673ac01c19814dd15fc617c02301c522f3d6812ca2024f4588ed4549/ruff-0.7.2-py3-none-win_arm64.whl", hash = "sha256:bb8368cd45bba3f57bb29cbb8d64b4a33f8415d0149d2655c5c8539452ce7760", size = 8735845, upload-time = "2024-11-01T15:07:24.629Z" }, ] [[package]] name = "setuptools" version = "80.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "tomli" version = "2.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096, upload-time = "2024-10-02T10:46:13.208Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237, upload-time = "2024-10-02T10:46:11.806Z" }, ] [[package]] name = "virtualenv" version = "20.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8c/b3/7b6a79c5c8cf6d90ea681310e169cf2db2884f4d583d16c6e1d5a75a4e04/virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", size = 6491145, upload-time = "2024-10-28T18:00:22.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ae/92/78324ff89391e00c8f4cf6b8526c41c6ef36b4ea2d2c132250b1a6fc2b8d/virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4", size = 3117838, upload-time = "2024-10-28T18:00:19.994Z" }, ] [[package]] name = "zipp" version = "3.23.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ]