├── .github └── workflows │ ├── ci.yml │ ├── codspeed.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cliff.toml ├── pyproject.toml ├── scripts ├── post-release.sh └── pre-release.sh ├── setup.py ├── src └── pytest_codspeed │ ├── __init__.py │ ├── config.py │ ├── instruments │ ├── __init__.py │ ├── hooks │ │ ├── __init__.py │ │ ├── build.py │ │ └── dist_instrument_hooks.pyi │ ├── valgrind.py │ └── walltime.py │ ├── plugin.py │ ├── py.typed │ └── utils.py ├── tests ├── benchmarks │ ├── TheAlgorithms_bench │ │ ├── __init__.py │ │ ├── bit_manipulation.py │ │ ├── test_bench_audio_filters.py │ │ └── test_bench_backtracking.py │ ├── __init__.py │ ├── test_bench_doc.py │ ├── test_bench_fibo.py │ ├── test_bench_misc.py │ ├── test_bench_syscalls.py │ └── test_bench_various_noop.py ├── conftest.py ├── examples │ ├── __init__.py │ └── test_addition_fixture.py ├── test_pytest_plugin.py ├── test_pytest_plugin_cpu_instrumentation.py ├── test_pytest_plugin_walltime.py └── test_utils.py └── uv.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | static-analysis: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: true 20 | - name: Set up Python 3.11 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.11" 24 | - uses: pre-commit/action@v3.0.1 25 | with: 26 | extra_args: --all-files 27 | 28 | tests: 29 | runs-on: ubuntu-latest 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | config: 34 | - headless 35 | - pytest-benchmark-4 36 | - pytest-benchmark-5 37 | - valgrind 38 | python-version: 39 | - "3.9" 40 | - "3.10" 41 | - "3.11" 42 | - "3.12" 43 | - "3.13" 44 | pytest-version: 45 | - ">=8.1.1" 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | submodules: true 51 | - uses: astral-sh/setup-uv@v4 52 | with: 53 | version: "0.5.20" 54 | - name: "Set up Python ${{ matrix.python-version }}" 55 | uses: actions/setup-python@v5 56 | with: 57 | python-version: "${{ matrix.python-version }}" 58 | - if: matrix.config == 'valgrind' || matrix.config == 'pytest-benchmark' 59 | name: Install valgrind 60 | run: | 61 | sudo apt-get update 62 | sudo apt-get install valgrind -y 63 | - name: Install dependencies with pytest${{ matrix.pytest-version }} 64 | run: | 65 | if [ "${{ matrix.config }}" == "valgrind" ]; then 66 | export PYTEST_CODSPEED_FORCE_EXTENSION_BUILD=1 67 | fi 68 | uv sync --all-extras --dev --locked --verbose 69 | uv pip install "pytest${{ matrix.pytest-version }}" 70 | uv pip uninstall pytest-benchmark 71 | - if: matrix.config == 'pytest-benchmark-4' 72 | name: Install pytest-benchmark 4.0.0 73 | run: uv pip install pytest-benchmark~=4.0.0 74 | - if: matrix.config == 'pytest-benchmark-5' 75 | name: Install pytest-benchmark 5.0.0 76 | run: uv pip install pytest-benchmark~=5.0.0 77 | - name: Run tests 78 | run: uv run --no-sync pytest -vs 79 | 80 | all-checks: 81 | runs-on: ubuntu-latest 82 | steps: 83 | - run: echo "All CI checks passed." 84 | needs: 85 | - static-analysis 86 | - tests 87 | -------------------------------------------------------------------------------- /.github/workflows/codspeed.yml: -------------------------------------------------------------------------------- 1 | name: CodSpeed 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | workflow_dispatch: 8 | 9 | env: 10 | PYTHON_VERSION: "3.12" 11 | 12 | jobs: 13 | benchmarks-instrumentation: 14 | strategy: 15 | matrix: 16 | include: 17 | - mode: "instrumentation" 18 | runs-on: ubuntu-24.04 19 | - mode: "walltime" 20 | runs-on: codspeed-macro 21 | 22 | name: Run ${{ matrix.mode }} benchmarks 23 | runs-on: ${{ matrix.runs-on }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | submodules: "recursive" 28 | - uses: actions/setup-python@v2 29 | with: 30 | python-version: ${{ env.PYTHON_VERSION }} 31 | - name: Install local version of pytest-codspeed 32 | run: | 33 | sudo apt-get update 34 | sudo apt-get install valgrind -y 35 | pip install . 36 | sudo apt-get remove valgrind -y 37 | - name: Run benchmarks 38 | uses: CodSpeedHQ/action@main 39 | with: 40 | run: pytest tests/benchmarks/ --codspeed 41 | token: ${{ secrets.CODSPEED_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release on tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | 9 | permissions: 10 | id-token: write 11 | contents: write 12 | 13 | jobs: 14 | build-wheels: 15 | strategy: 16 | matrix: 17 | platform: 18 | - runs-on: ubuntu-24.04 19 | arch: x86_64 20 | - runs-on: buildjet-8vcpu-ubuntu-2204-arm 21 | arch: aarch64 22 | 23 | runs-on: ${{ matrix.platform.runs-on }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | submodules: true 28 | - name: Build wheels 29 | uses: pypa/cibuildwheel@v2.22.0 30 | env: 31 | CIBW_ARCHS: ${{ matrix.platform.arch }} 32 | with: 33 | output-dir: wheelhouse 34 | 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | name: wheels-${{ matrix.platform.arch }} 38 | path: wheelhouse/*.whl 39 | 40 | build-py3-none-any: 41 | runs-on: ubuntu-24.04 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | submodules: true 46 | - uses: astral-sh/setup-uv@v4 47 | with: 48 | version: "0.5.20" 49 | - uses: actions/setup-python@v2 50 | with: 51 | python-version: "3.12" 52 | - name: Build py3-none-any wheel 53 | env: 54 | PYTEST_CODSPEED_SKIP_EXTENSION_BUILD: "1" 55 | run: uv build --wheel --out-dir dist/ 56 | 57 | - uses: actions/upload-artifact@v4 58 | with: 59 | name: wheels-py3-none-any 60 | path: dist/*.whl 61 | 62 | build-sdist: 63 | runs-on: ubuntu-24.04 64 | steps: 65 | - uses: actions/checkout@v4 66 | with: 67 | submodules: true 68 | - uses: astral-sh/setup-uv@v4 69 | with: 70 | version: "0.5.20" 71 | - uses: actions/setup-python@v2 72 | with: 73 | python-version: "3.12" 74 | - name: Build the source dist 75 | run: uv build --sdist --out-dir dist/ 76 | 77 | - uses: actions/upload-artifact@v4 78 | with: 79 | name: sdist 80 | path: dist/*.tar.gz 81 | 82 | publish: 83 | needs: 84 | - build-wheels 85 | - build-py3-none-any 86 | - build-sdist 87 | 88 | runs-on: ubuntu-24.04 89 | steps: 90 | - uses: actions/download-artifact@v4 91 | with: 92 | merge-multiple: true 93 | path: dist/ 94 | - uses: actions/checkout@v4 95 | - uses: astral-sh/setup-uv@v4 96 | with: 97 | version: "0.5.20" 98 | - uses: actions/setup-python@v2 99 | with: 100 | python-version: "3.12" 101 | 102 | - uses: actions/download-artifact@v4 103 | with: 104 | merge-multiple: true 105 | path: dist/ 106 | 107 | - name: List artifacts 108 | run: ls -al dist/* 109 | 110 | - if: github.event_name == 'push' 111 | name: Publish to PyPI 112 | run: uv publish --trusted-publishing=always dist/* 113 | 114 | - if: github.event_name == 'push' 115 | name: Create a draft release 116 | run: | 117 | VERSION="${{ github.ref_name }}" 118 | gh release create $VERSION --title $VERSION --generate-notes -d 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | .venv.* 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | #.idea/ 162 | 163 | .venvubuntu 164 | .python-version 165 | *.o 166 | 167 | .codspeed 168 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/benchmarks/TheAlgorithms"] 2 | path = tests/benchmarks/TheAlgorithms 3 | url = git@github.com:TheAlgorithms/Python.git 4 | [submodule "src/pytest_codspeed/instruments/hooks/instrument-hooks"] 5 | path = src/pytest_codspeed/instruments/hooks/instrument-hooks 6 | url = https://github.com/CodSpeedHQ/instrument-hooks 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/pre-commit/mirrors-mypy 12 | rev: v1.3.0 13 | hooks: 14 | - id: mypy 15 | - repo: https://github.com/astral-sh/ruff-pre-commit 16 | rev: v0.11.12 17 | hooks: 18 | - id: ruff-check 19 | args: [--fix] 20 | - id: ruff-format 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug walltime benchmarks", 6 | "type": "debugpy", 7 | "request": "launch", 8 | "module": "pytest", 9 | "args": ["--codspeed", "--codspeed-mode", "walltime", "tests/benchmarks"] 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": ["tests"], 3 | "python.testing.unittestEnabled": false, 4 | "python.testing.pytestEnabled": true 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | ## [Unreleased] 6 | 7 | ### ⚙️ Internals 8 | 9 | - Fix changelog generation by @art049 10 | 11 | ## [4.0.0] - 2025-07-10 12 | 13 | ### 🚀 Features 14 | 15 | - Update readme by @art049 16 | 17 | ### ⚙️ Internals 18 | 19 | - Remove pre-releases from git-cliff changelog by @art049 20 | - Link to the documentation by @art049 21 | - Improve reliability of perf trampoline compatibility checks by @art049 22 | 23 | ## [4.0.0-beta1] - 2025-06-10 24 | 25 | ### 🐛 Bug Fixes 26 | 27 | - Reenable walltime instrument hooks by @art049 in [#82](https://github.com/CodSpeedHQ/pytest-codspeed/pull/82) 28 | 29 | ## [4.0.0-beta] - 2025-06-06 30 | 31 | ### 🚀 Features 32 | 33 | - Support pytest-benchmark's pedantic API by @art049 in [#81](https://github.com/CodSpeedHQ/pytest-codspeed/pull/81) 34 | - Make sure the benchmark fixture can only be called once per bench by @art049 35 | - Support marker attributes to customize the walltime execution by @art049 in [#80](https://github.com/CodSpeedHQ/pytest-codspeed/pull/80) 36 | - Use instrument hooks by @not-matthias 37 | - Add instrument-hooks native module by @not-matthias 38 | 39 | ### 🐛 Bug Fixes 40 | 41 | - Fix native library typing by @art049 42 | 43 | ### 🧪 Testing 44 | 45 | - Add benches from the documentation's getting started by @art049 in [#71](https://github.com/CodSpeedHQ/pytest-codspeed/pull/71) 46 | - Add simple python benches by @art049 47 | 48 | ### ⚙️ Internals 49 | 50 | - Bump ruff by @art049 51 | - Update release workflow to include submodules by @art049 in [#79](https://github.com/CodSpeedHQ/pytest-codspeed/pull/79) 52 | - Remove valgrind wrapper by @not-matthias 53 | - Update apt before installing packages by @art049 54 | 55 | ## [3.2.0] - 2025-01-31 56 | 57 | ### 🚀 Features 58 | 59 | - Increase the min round time to a bigger value (+/- 1ms) by @art049 60 | - Add benchmarks-walltime job to run additional performance benchmarks by @art049 in [#65](https://github.com/CodSpeedHQ/pytest-codspeed/pull/65) 61 | - Fix the random seed while measuring with instruments by @art049 in [#48](https://github.com/CodSpeedHQ/pytest-codspeed/pull/48) 62 | 63 | ### 🐛 Bug Fixes 64 | 65 | - Use time per iteration instead of total round time in stats by @art049 66 | 67 | ### 🏗️ Refactor 68 | 69 | - Replace hardcoded outlier factor for improved readability by @art049 in [#67](https://github.com/CodSpeedHQ/pytest-codspeed/pull/67) 70 | 71 | ### ⚙️ Internals 72 | 73 | - Fix self-dependency by @adriencaccia in [#66](https://github.com/CodSpeedHQ/pytest-codspeed/pull/66) 74 | - Fix uv version in CI by @adriencaccia 75 | 76 | ## [3.1.2] - 2025-01-09 77 | 78 | ### 🐛 Bug Fixes 79 | 80 | - Update package_data to include header and source files for valgrind wrapper by @art049 in [#64](https://github.com/CodSpeedHQ/pytest-codspeed/pull/64) 81 | 82 | ## [3.1.1] - 2025-01-07 83 | 84 | ### ⚙️ Internals 85 | 86 | - Fix tag num with bumpver by @art049 in [#61](https://github.com/CodSpeedHQ/pytest-codspeed/pull/61) 87 | - Update uv lock before release by @art049 88 | - Add a py3-none-any fallback wheel by @art049 89 | 90 | ## [3.1.0] - 2024-12-09 91 | 92 | ### 🏗️ Refactor 93 | 94 | - Remove the scripted semver generation by @art049 95 | 96 | ### ⚙️ Internals 97 | 98 | - Fix typo in cibuildwheel config by @art049 in [#57](https://github.com/CodSpeedHQ/pytest-codspeed/pull/57) 99 | 100 | ## [3.1.0-beta] - 2024-12-06 101 | 102 | ### 🚀 Features 103 | 104 | - Check buildability and fallback when build doesn't work by @art049 105 | - Compile the callgrind wrapper at build time by @art049 106 | 107 | ### 🐛 Bug Fixes 108 | 109 | - Allow build on arm64 by @art049 110 | 111 | ### ⚙️ Internals 112 | 113 | - Build wheels with cibuildwheel by @art049 114 | - Allow forcing integrated tests by @art049 115 | - Fix release script by @art049 116 | - Use bumpver to manage versions by @art049 117 | - Add a changelog by @art049 118 | - Force native extension build in CI by @art049 119 | - Updated matrix release workflow by @art049 120 | - Use a common python version in the codspeed job by @art049 121 | - Fix the codspeed workflow by @art049 122 | - Use uv in CI by @art049 123 | - Commit uv lock file by @art049 124 | 125 | ## [3.0.0] - 2024-10-29 126 | 127 | ### 🐛 Bug Fixes 128 | 129 | - Fix compatibility with pytest-benchmark 5.0.0 by @art049 in [#54](https://github.com/CodSpeedHQ/pytest-codspeed/pull/54) 130 | 131 | ### ⚙️ Internals 132 | 133 | - Drop support for python3.8 by @art049 134 | - Expose type information (#53) by @Dreamsorcerer in [#53](https://github.com/CodSpeedHQ/pytest-codspeed/pull/53) 135 | - Run the CI with ubuntu 24.04 by @art049 136 | - Improve naming in workflow examples by @art049 137 | - Bump actions/checkout to v4 (#47) by @fargito in [#47](https://github.com/CodSpeedHQ/pytest-codspeed/pull/47) 138 | 139 | ## [3.0.0b4] - 2024-09-27 140 | 141 | ### 🚀 Features 142 | 143 | - Send more outlier data by @art049 144 | 145 | ### 🐛 Bug Fixes 146 | 147 | - Fix display of parametrized tests by @art049 148 | - Reenable gc logic by @art049 149 | 150 | ### 🧪 Testing 151 | 152 | - Add benches for various syscalls by @art049 153 | 154 | ## [3.0.0b3] - 2024-09-26 155 | 156 | ### 🚀 Features 157 | 158 | - Also save the lower and upper fences in the json data by @art049 in [#46](https://github.com/CodSpeedHQ/pytest-codspeed/pull/46) 159 | 160 | ### 🧪 Testing 161 | 162 | - Refactor the algorithm benches using parametrization and add benches on bit_manipulation by @art049 163 | 164 | ## [3.0.0b2] - 2024-09-24 165 | 166 | ### 🚀 Features 167 | 168 | - Also save the q1 and q3 in the json data by @art049 in [#45](https://github.com/CodSpeedHQ/pytest-codspeed/pull/45) 169 | - Add the --codspeed-max-time flag by @art049 170 | 171 | ## [3.0.0b1] - 2024-09-20 172 | 173 | ### 🚀 Features 174 | 175 | - Send the semver version to cospeed instead of the PEP440 one by @art049 in [#44](https://github.com/CodSpeedHQ/pytest-codspeed/pull/44) 176 | - Also store the semver version by @art049 177 | 178 | ### 🧪 Testing 179 | 180 | - Add benches for TheAlgorithms/backtracking by @art049 in [#43](https://github.com/CodSpeedHQ/pytest-codspeed/pull/43) 181 | 182 | ## [3.0.0b0] - 2024-09-18 183 | 184 | ### 🚀 Features 185 | 186 | - Improve table style when displaying results by @art049 in [#41](https://github.com/CodSpeedHQ/pytest-codspeed/pull/41) 187 | - Add the total bench time to the collected stats by @art049 188 | - Add configuration and split tests between instruments by @art049 189 | - Add outlier detection in the walltime instrument by @art049 190 | - Implement the walltime instrument by @art049 191 | - Add bench of various python noop by @art049 192 | - Avoid overriding pytest's default protocol (#32) by @kenodegard in [#32](https://github.com/CodSpeedHQ/pytest-codspeed/pull/32) 193 | 194 | ### 🐛 Bug Fixes 195 | 196 | - Use importlib_metadata to keep backward compatibility by @art049 197 | - Properly decide the mode depending on our env variable spec by @art049 198 | - Disable pytest-speed when installed and codspeed is enabled by @art049 199 | 200 | ### 🏗️ Refactor 201 | 202 | - Differentiate the mode from the underlying instrument by @art049 203 | - Move the instrumentation wrapper directly in the instrument by @art049 204 | - Change Instrumentation to CPUInstrumentation by @art049 205 | - Create an abstraction for each instrument by @art049 206 | 207 | ### 📚 Documentation 208 | 209 | - Update action version in the CI workflow configuration (#39) by @frgfm in [#39](https://github.com/CodSpeedHQ/pytest-codspeed/pull/39) 210 | - Bump action versions in README by @adriencaccia 211 | 212 | ### 🧪 Testing 213 | 214 | - Add benches for TheAlgorithms/audio_filters by @art049 in [#42](https://github.com/CodSpeedHQ/pytest-codspeed/pull/42) 215 | 216 | ### ⚙️ Internals 217 | 218 | - Add a test on the walltime instrument by @art049 219 | - Fix utils test using a fake git repo by @art049 220 | - Update readme by @art049 221 | - Support python 3.13 and drop 3.7 by @art049 in [#40](https://github.com/CodSpeedHQ/pytest-codspeed/pull/40) 222 | - Add TCH, FA, and UP to ruff lints (#29) by @kenodegard in [#29](https://github.com/CodSpeedHQ/pytest-codspeed/pull/29) 223 | 224 | ## [2.2.1] - 2024-03-19 225 | 226 | ### 🚀 Features 227 | 228 | - Support pytest 8.1.1 by @art049 229 | 230 | ### 🐛 Bug Fixes 231 | 232 | - Loosen runtime requirements (#21) by @edgarrmondragon in [#21](https://github.com/CodSpeedHQ/pytest-codspeed/pull/21) 233 | 234 | ### ⚙️ Internals 235 | 236 | - Add all-checks job to CI workflow by @art049 in [#28](https://github.com/CodSpeedHQ/pytest-codspeed/pull/28) 237 | - Switch from black to ruff format by @art049 238 | - Update action version in README.md by @adriencaccia 239 | - Add codspeed badge to the readme by @art049 240 | 241 | ## [2.2.0] - 2023-09-01 242 | 243 | ### 🚀 Features 244 | 245 | - Avoid concurrent wrapper builds by @art049 246 | - Add a test for pytest-xdist compatibility by @art049 247 | 248 | ### 🐛 Bug Fixes 249 | 250 | - Fix xdist test output assertion by @art049 251 | 252 | ## [2.1.0] - 2023-07-27 253 | 254 | ### 🐛 Bug Fixes 255 | 256 | - Fix relative git path when using working-directory by @art049 in [#15](https://github.com/CodSpeedHQ/pytest-codspeed/pull/15) 257 | - Fix typo in release.yml (#14) by @art049 in [#14](https://github.com/CodSpeedHQ/pytest-codspeed/pull/14) 258 | 259 | ## [2.0.1] - 2023-07-22 260 | 261 | ### 🚀 Features 262 | 263 | - Release the package from the CI with trusted provider by @art049 264 | - Add a return type to the benchmark fixture by @art049 in [#13](https://github.com/CodSpeedHQ/pytest-codspeed/pull/13) 265 | - Add support for returning values (#12) by @patrick91 in [#12](https://github.com/CodSpeedHQ/pytest-codspeed/pull/12) 266 | 267 | ### 🐛 Bug Fixes 268 | 269 | - Fix setuptools installation with python3.12 by @art049 270 | 271 | ## [2.0.0] - 2023-07-04 272 | 273 | ### 🚀 Features 274 | 275 | - Warmup performance map generation by @art049 276 | - Add some details about the callgraph generation status in the header by @art049 277 | - Test that perf maps are generated by @art049 278 | - Add a local test matrix with hatch by @art049 279 | - Test that benchmark selection with -k works by @art049 280 | - Add support for CPython3.12 and perf trampoline by @art049 281 | - Add introspection benchmarks by @art049 in [#9](https://github.com/CodSpeedHQ/pytest-codspeed/pull/9) 282 | 283 | ### 🐛 Bug Fixes 284 | 285 | - Support benchmark.extra_info parameters on the fixture by @art049 in [#10](https://github.com/CodSpeedHQ/pytest-codspeed/pull/10) 286 | - Filter out pytest-benchmark warnings in the tests by @art049 287 | 288 | ### 🏗️ Refactor 289 | 290 | - Use the pytest_run_protocol hook for better exec control by @art049 291 | 292 | ### ⚙️ Internals 293 | 294 | - Separate the benchmark workflow by @art049 in [#8](https://github.com/CodSpeedHQ/pytest-codspeed/pull/8) 295 | - Bump version to 1.3.0 to trigger the callgraph generation by @art049 296 | - Reuse same test code in the tests by @art049 297 | - Bump linting dependencies by @art049 298 | - Bump precommit in the CI by @art049 299 | - Add python3.12 to the ci matrix by @art049 300 | - Restructure dev dependencies by @art049 301 | - Replace isort by ruff by @art049 in [#11](https://github.com/CodSpeedHQ/pytest-codspeed/pull/11) 302 | - Add discord badge in the readme by @art049 303 | 304 | ## [1.2.2] - 2022-12-02 305 | 306 | ### 🚀 Features 307 | 308 | - Add library metadata in the profile output by @art049 in [#5](https://github.com/CodSpeedHQ/pytest-codspeed/pull/5) 309 | 310 | ## [1.2.1] - 2022-11-28 311 | 312 | ### 🐛 Bug Fixes 313 | 314 | - Support kwargs with the benchmark fixture by @art049 in [#4](https://github.com/CodSpeedHQ/pytest-codspeed/pull/4) 315 | 316 | ## [1.2.0] - 2022-11-22 317 | 318 | ### 🐛 Bug Fixes 319 | 320 | - Avoid wrapping the callable to maintain existing results by @art049 321 | - Disable automatic garbage collection to increase stability by @art049 in [#2](https://github.com/CodSpeedHQ/pytest-codspeed/pull/2) 322 | - Update readme by @art049 323 | 324 | ### ⚙️ Internals 325 | 326 | - Update readme by @art049 327 | 328 | ## [1.1.0] - 2022-11-10 329 | 330 | ### 🚀 Features 331 | 332 | - Allow running along with pytest-benchmarks by @art049 333 | 334 | ### 🐛 Bug Fixes 335 | 336 | - Fix the release script by @art049 337 | - Make the release script executable by @art049 338 | - Match the test output in any order by @art049 339 | 340 | ### 🏗️ Refactor 341 | 342 | - Manage compatibility env in the conftest by @art049 343 | 344 | ### ⚙️ Internals 345 | 346 | - Add a pytest-benchmark compatibility test by @art049 in [#1](https://github.com/CodSpeedHQ/pytest-codspeed/pull/1) 347 | - Add more details on the pytest run by @art049 348 | - Continue running on matrix item error by @art049 349 | - Add a CI configuration with pytest-benchmark installed by @art049 350 | 351 | ## [1.0.1] - 2022-11-05 352 | 353 | [unreleased]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v4.0.0..HEAD 354 | [4.0.0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v4.0.0-beta1..v4.0.0 355 | [4.0.0-beta1]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v4.0.0-beta..v4.0.0-beta1 356 | [4.0.0-beta]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.2.0..v4.0.0-beta 357 | [3.2.0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.1.2..v3.2.0 358 | [3.1.2]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.1.1..v3.1.2 359 | [3.1.1]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.1.0..v3.1.1 360 | [3.1.0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.1.0-beta..v3.1.0 361 | [3.1.0-beta]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.0.0..v3.1.0-beta 362 | [3.0.0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.0.0b4..v3.0.0 363 | [3.0.0b4]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.0.0b3..v3.0.0b4 364 | [3.0.0b3]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.0.0b2..v3.0.0b3 365 | [3.0.0b2]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.0.0b1..v3.0.0b2 366 | [3.0.0b1]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v3.0.0b0..v3.0.0b1 367 | [3.0.0b0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v2.2.1..v3.0.0b0 368 | [2.2.1]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v2.2.0..v2.2.1 369 | [2.2.0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v2.1.0..v2.2.0 370 | [2.1.0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v2.0.1..v2.1.0 371 | [2.0.1]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v2.0.0..v2.0.1 372 | [2.0.0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v1.2.2..v2.0.0 373 | [1.2.2]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v1.2.1..v1.2.2 374 | [1.2.1]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v1.2.0..v1.2.1 375 | [1.2.0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v1.1.0..v1.2.0 376 | [1.1.0]: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v1.0.4..v1.1.0 377 | 378 | 379 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 CodSpeed and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

pytest-codspeed

3 | 4 | [![CI](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml) 5 | [![PyPi Version](https://img.shields.io/pypi/v/pytest-codspeed?color=%2334D058&label=pypi)](https://pypi.org/project/pytest-codspeed) 6 | ![Python Version](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12%20|%203.13-informational.svg) 7 | [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF) 8 | [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/pytest-codspeed) 9 | 10 | Pytest plugin to create CodSpeed benchmarks 11 | 12 |
13 | 14 | --- 15 | 16 | **Documentation**: https://codspeed.io/docs/reference/pytest-codspeed 17 | 18 | --- 19 | 20 | ## Installation 21 | 22 | ```shell 23 | pip install pytest-codspeed 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Creating benchmarks 29 | 30 | In a nutshell, `pytest-codspeed` offers two approaches to create performance benchmarks that integrate seamlessly with your existing test suite. 31 | 32 | Use `@pytest.mark.benchmark` to measure entire test functions automatically: 33 | 34 | ```python 35 | import pytest 36 | from statistics import median 37 | 38 | @pytest.mark.benchmark 39 | def test_median_performance(): 40 | input = [1, 2, 3, 4, 5] 41 | output = sum(i**2 for i in input) 42 | assert output == 55 43 | ``` 44 | 45 | Since this measure the entire function, you might want to use the `benchmark` fixture for precise control over what code gets measured: 46 | 47 | ```python 48 | def test_mean_performance(benchmark): 49 | data = [1, 2, 3, 4, 5] 50 | # Only the function call is measured 51 | result = benchmark(lambda: sum(i**2 for i in data)) 52 | assert result == 55 53 | ``` 54 | 55 | Check out the [full documentation](https://codspeed.io/docs/reference/pytest-codspeed) for more details. 56 | 57 | ### Testing the benchmarks locally 58 | 59 | If you want to run the benchmarks tests locally, you can use the `--codspeed` pytest flag: 60 | 61 | ```sh 62 | $ pytest tests/ --codspeed 63 | ============================= test session starts ==================== 64 | platform darwin -- Python 3.13.0, pytest-7.4.4, pluggy-1.5.0 65 | codspeed: 3.0.0 (enabled, mode: walltime, timer_resolution: 41.7ns) 66 | rootdir: /home/user/codspeed-test, configfile: pytest.ini 67 | plugins: codspeed-3.0.0 68 | collected 1 items 69 | 70 | tests/test_sum_squares.py . [ 100%] 71 | 72 | Benchmark Results 73 | ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━┓ 74 | ┃ Benchmark ┃ Time (best) ┃ Rel. StdDev ┃ Run time ┃ Iters ┃ 75 | ┣━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━╋━━━━━━━━━━━━━╋━━━━━━━━━━╋━━━━━━━━┫ 76 | ┃test_sum_squares┃ 1,873ns ┃ 4.8% ┃ 3.00s ┃ 66,930 ┃ 77 | ┗━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┻━━━━━━━━━━━━━┻━━━━━━━━━━┻━━━━━━━━┛ 78 | =============================== 1 benchmarked ======================== 79 | =============================== 1 passed in 4.12s ==================== 80 | ``` 81 | 82 | ### Running the benchmarks in your CI 83 | 84 | You can use the [CodSpeedHQ/action](https://github.com/CodSpeedHQ/action) to run the benchmarks in Github Actions and upload the results to CodSpeed. 85 | 86 | Here is an example of a GitHub Actions workflow that runs the benchmarks and reports the results to CodSpeed on every push to the `main` branch and every pull request: 87 | 88 | ```yaml 89 | name: CodSpeed 90 | 91 | on: 92 | push: 93 | branches: 94 | - "main" # or "master" 95 | pull_request: 96 | # `workflow_dispatch` allows CodSpeed to trigger backtest 97 | # performance analysis in order to generate initial data. 98 | workflow_dispatch: 99 | 100 | jobs: 101 | benchmarks: 102 | name: Run benchmarks 103 | runs-on: ubuntu-latest 104 | steps: 105 | - uses: actions/checkout@v4 106 | - uses: actions/setup-python@v5 107 | with: 108 | python-version: "3.13" 109 | 110 | - name: Install dependencies 111 | run: pip install -r requirements.txt 112 | 113 | - name: Run benchmarks 114 | uses: CodSpeedHQ/action@v3 115 | with: 116 | token: ${{ secrets.CODSPEED_TOKEN }} 117 | run: pytest tests/ --codspeed 118 | ``` 119 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | [remote.github] 8 | owner = "CodSpeedHQ" 9 | repo = "pytest-codspeed" 10 | 11 | [changelog] 12 | # template for the changelog header 13 | header = """ 14 | # Changelog\n 15 | 16 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 17 | \n 18 | 19 | """ 20 | # template for the changelog body 21 | # https://keats.github.io/tera/docs/#introduction 22 | body = """ 23 | {%- macro remote_url() -%} 24 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 25 | {%- endmacro -%} 26 | 27 | {% if version -%} 28 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 29 | {% else -%} 30 | ## [Unreleased] 31 | {% endif -%} 32 | 33 | {% for group, commits in commits | group_by(attribute="group") %} 34 | ### {{ group | upper_first }} 35 | {%- for commit in commits %} 36 | - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\ 37 | {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%} 38 | {% if commit.remote.pr_number %} in \ 39 | [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \ 40 | {%- endif -%} 41 | {% endfor %} 42 | {% endfor %}\n\n 43 | """ 44 | # template for the changelog footer 45 | footer = """ 46 | {%- macro remote_url() -%} 47 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 48 | {%- endmacro -%} 49 | 50 | {% for release in releases -%} 51 | {% if release.version -%} 52 | {% if release.previous.version -%} 53 | [{{ release.version | trim_start_matches(pat="v") }}]: \ 54 | {{ self::remote_url() }}/compare/{{ release.previous.version }}..{{ release.version }} 55 | {% endif -%} 56 | {% else -%} 57 | [unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}..HEAD 58 | {% endif -%} 59 | {% endfor %} 60 | 61 | """ 62 | 63 | # remove the leading and trailing s 64 | trim = true 65 | # postprocessors 66 | postprocessors = [ 67 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 68 | ] 69 | # render body even when there are no releases to process 70 | # render_always = true 71 | # output file path 72 | # output = "test.md" 73 | 74 | [git] 75 | # parse the commits based on https://www.conventionalcommits.org 76 | conventional_commits = true 77 | # filter out the commits that are not conventional 78 | filter_unconventional = true 79 | # process each line of a commit as an individual commit 80 | split_commits = false 81 | # regex for preprocessing the commit messages 82 | commit_preprocessors = [ 83 | # Replace issue numbers 84 | #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, 85 | # Check spelling of the commit with https://github.com/crate-ci/typos 86 | # If the spelling is incorrect, it will be automatically fixed. 87 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }, 88 | ] 89 | # regex for parsing and grouping commits 90 | commit_parsers = [ 91 | { message = "^feat", group = "🚀 Features" }, 92 | { message = "^fix", group = "🐛 Bug Fixes" }, 93 | { message = "^doc", group = "📚 Documentation" }, 94 | { message = "^perf", group = "⚡ Performance" }, 95 | { message = "^refactor", group = "🏗️ Refactor" }, 96 | { message = "^style", group = "🎨 Styling" }, 97 | { message = "^test", group = "🧪 Testing" }, 98 | { message = "^chore\\(release\\): prepare for", skip = true }, 99 | { message = "^chore: Release", skip = true }, 100 | { message = "^chore\\(deps.*\\)", skip = true }, 101 | { message = "^chore\\(pr\\)", skip = true }, 102 | { message = "^chore\\(pull\\)", skip = true }, 103 | { message = "^chore|^ci", group = "⚙️ Internals" }, 104 | { body = ".*security", group = "🛡️ Security" }, 105 | { message = "^revert", group = "◀️ Revert" }, 106 | { message = ".*", group = "💼 Other" }, 107 | ] 108 | # filter out the commits that are not matched by commit parsers 109 | filter_commits = false 110 | # sort the tags topologically 111 | topo_order = false 112 | # sort the commits inside sections by oldest/newest order 113 | sort_commits = "newest" 114 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project.urls] 2 | Homepage = "https://codspeed.io/" 3 | Documentation = "https://codspeed.io/docs/reference/pytest-codspeed" 4 | Source = "https://github.com/CodSpeedHQ/pytest-codspeed" 5 | 6 | [project] 7 | name = "pytest-codspeed" 8 | dynamic = ["version"] 9 | description = "Pytest plugin to create CodSpeed benchmarks" 10 | readme = "README.md" 11 | license = { file = "LICENSE" } 12 | requires-python = ">=3.9" 13 | authors = [{ name = "Arthur Pastel", email = "arthur@codspeed.io" }] 14 | keywords = ["codspeed", "benchmark", "performance", "pytest"] 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Framework :: Pytest", 18 | "Intended Audience :: Developers", 19 | "Intended Audience :: Information Technology", 20 | "License :: OSI Approved :: MIT License", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | "Topic :: Software Development :: Testing", 28 | "Topic :: System :: Benchmark", 29 | "Topic :: Utilities", 30 | "Typing :: Typed", 31 | ] 32 | dependencies = [ 33 | "cffi >= 1.17.1", 34 | "pytest>=3.8", 35 | "rich>=13.8.1", 36 | "importlib-metadata>=8.5.0; python_version < '3.10'", 37 | ] 38 | 39 | [project.optional-dependencies] 40 | lint = ["mypy ~= 1.11.2", "ruff ~= 0.11.12"] 41 | compat = [ 42 | "pytest-benchmark ~= 5.0.0", 43 | "pytest-xdist ~= 3.6.1", 44 | # "pytest-speed>=0.3.5", 45 | ] 46 | test = ["pytest ~= 7.0", "pytest-cov ~= 4.0.0"] 47 | 48 | [tool.uv.sources] 49 | pytest-codspeed = { workspace = true } 50 | 51 | [dependency-groups] 52 | dev = ["pytest-codspeed"] 53 | 54 | [project.entry-points] 55 | pytest11 = { codspeed = "pytest_codspeed.plugin" } 56 | 57 | [build-system] 58 | requires = ["setuptools >= 61", "cffi >= 1.17.1"] 59 | build-backend = "setuptools.build_meta" 60 | 61 | [tool.setuptools] 62 | license-files = [] # Workaround of https://github.com/astral-sh/uv/issues/9513 63 | 64 | [tool.setuptools.dynamic] 65 | version = { attr = "pytest_codspeed.__version__" } 66 | 67 | 68 | [tool.bumpver] 69 | current_version = "4.0.0" 70 | version_pattern = "MAJOR.MINOR.PATCH[-TAG[NUM]]" 71 | commit_message = "Release v{new_version} 🚀" 72 | tag_message = "Release v{new_version} 🚀" 73 | tag_scope = "default" 74 | allow_dirty = false 75 | pre_commit_hook = "./scripts/pre-release.sh" 76 | post_commit_hook = "./scripts/post-release.sh" 77 | commit = true 78 | tag = false 79 | push = false 80 | 81 | 82 | [tool.bumpver.file_patterns] 83 | "pyproject.toml" = ['current_version = "{version}"'] 84 | "src/pytest_codspeed/__init__.py" = [ 85 | '__version__ = "{pep440_version}"', 86 | '__semver_version__ = "{version}"', 87 | ] 88 | 89 | [tool.cibuildwheel] 90 | build = "cp*manylinux*" 91 | test-extras = ["build", "test", "compat"] 92 | test-command = "pytest -v --ignore={project}/tests/benchmarks {project}/tests" 93 | 94 | [tool.cibuildwheel.linux] 95 | environment = { PYTEST_CODSPEED_FORCE_EXTENSION_BUILD = "1", PYTEST_CODSPEED_FORCE_VALGRIND_TESTS = "1" } 96 | manylinux-x86_64-image = "manylinux_2_28" 97 | manylinux-aarch64-image = "manylinux_2_28" 98 | before-all = "yum -y install valgrind-devel" 99 | 100 | [tool.mypy] 101 | python_version = "3.12" 102 | 103 | [tool.ruff] 104 | target-version = "py37" 105 | 106 | [tool.ruff.lint] 107 | select = ["E", "F", "I", "C", "TCH", "FA", "UP"] 108 | flake8-type-checking = { exempt-modules = [], strict = true } 109 | 110 | [tool.isort] 111 | line_length = 88 112 | multi_line_output = 3 113 | include_trailing_comma = true 114 | use_parentheses = true 115 | force_grid_wrap = 0 116 | float_to_top = true 117 | 118 | [tool.pytest.ini_options] 119 | addopts = "--ignore=tests/benchmarks --ignore=tests/examples --ignore=tests/benchmarks/TheAlgorithms" 120 | filterwarnings = ["ignore::DeprecationWarning:pytest_benchmark.utils.*:"] 121 | pythonpath = ["tests/benchmarks/TheAlgorithms", "./scripts"] 122 | 123 | [tool.coverage.run] 124 | branch = true 125 | [tool.coverage.report] 126 | include = ["src/*", "tests/*"] 127 | omit = ["**/conftest.py"] 128 | exclude_lines = [ 129 | "pragma: no cover", 130 | "if TYPE_CHECKING:", 131 | "@pytest.mark.skip", 132 | "@abstractmethod", 133 | ] 134 | -------------------------------------------------------------------------------- /scripts/post-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | VERSION=$BUMPVER_NEW_VERSION 5 | 6 | # We handle tagging here since bumpver doesn't allow custom 7 | # tagnames and we want a v prefix 8 | git tag v$VERSION -m "Release v$VERSION 🚀" 9 | git push --follow-tags 10 | -------------------------------------------------------------------------------- /scripts/pre-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | VERSION=v$BUMPVER_NEW_VERSION 5 | 6 | # Skip alpha/beta/rc changelog generation 7 | if [[ $VERSION == *"alpha"* ]] || [[ $VERSION == *"beta"* ]] || [[ $VERSION == *"rc"* ]]; then 8 | echo "Skipping changelog generation for alpha/beta/rc release" 9 | else 10 | echo "Generating changelog for $VERSION" 11 | # Check that GITHUB_TOKEN is set 12 | if [ -z "$GITHUB_TOKEN" ]; then 13 | echo "GITHUB_TOKEN is not set. Trying to fetch it from gh" 14 | GITHUB_TOKEN=$(gh auth token) 15 | fi 16 | git cliff --unreleased --tag $VERSION --prepend CHANGELOG.md 17 | git add CHANGELOG.md 18 | fi 19 | 20 | uv lock 21 | git add uv.lock 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import os 3 | import platform 4 | from pathlib import Path 5 | 6 | from setuptools import setup 7 | 8 | build_path = Path(__file__).parent / "src/pytest_codspeed/instruments/hooks/build.py" 9 | 10 | spec = importlib.util.spec_from_file_location("build", build_path) 11 | assert spec is not None, "The spec should be initialized" 12 | build = importlib.util.module_from_spec(spec) 13 | assert spec.loader is not None, "The loader should be initialized" 14 | spec.loader.exec_module(build) 15 | 16 | system = platform.system() 17 | current_arch = platform.machine() 18 | 19 | print(f"System: {system} ({current_arch})") 20 | 21 | IS_EXTENSION_BUILDABLE = system == "Linux" and current_arch in [ 22 | "x86_64", 23 | "aarch64", 24 | ] 25 | 26 | IS_EXTENSION_REQUIRED = ( 27 | os.environ.get("PYTEST_CODSPEED_FORCE_EXTENSION_BUILD") is not None 28 | ) 29 | 30 | SKIP_EXTENSION_BUILD = ( 31 | os.environ.get("PYTEST_CODSPEED_SKIP_EXTENSION_BUILD") is not None 32 | ) 33 | 34 | if SKIP_EXTENSION_BUILD and IS_EXTENSION_REQUIRED: 35 | raise ValueError("Extension build required but the build requires to skip it") 36 | 37 | if IS_EXTENSION_REQUIRED and not IS_EXTENSION_BUILDABLE: 38 | raise ValueError( 39 | "The extension is required but the current platform is not supported" 40 | ) 41 | 42 | ffi_extension = build.ffibuilder.distutils_extension() 43 | ffi_extension.optional = not IS_EXTENSION_REQUIRED 44 | 45 | print( 46 | "CodSpeed native extension is " 47 | + ("required" if IS_EXTENSION_REQUIRED else "optional") 48 | ) 49 | 50 | setup( 51 | package_data={ 52 | "pytest_codspeed": [ 53 | "instruments/hooks/instrument-hooks/includes/*.h", 54 | "instruments/hooks/instrument-hooks/dist/*.c", 55 | ] 56 | }, 57 | ext_modules=( 58 | [ffi_extension] if IS_EXTENSION_BUILDABLE and not SKIP_EXTENSION_BUILD else [] 59 | ), 60 | ) 61 | -------------------------------------------------------------------------------- /src/pytest_codspeed/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.0.0" 2 | # We also have the semver version since __version__ is not semver compliant 3 | __semver_version__ = "4.0.0" 4 | 5 | from .plugin import BenchmarkFixture 6 | 7 | __all__ = ["BenchmarkFixture", "__version__", "__semver_version__"] 8 | -------------------------------------------------------------------------------- /src/pytest_codspeed/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import dataclasses 4 | from dataclasses import dataclass, field 5 | from typing import TYPE_CHECKING, Generic, TypeVar 6 | 7 | T = TypeVar("T") 8 | 9 | if TYPE_CHECKING: 10 | from typing import Any, Callable 11 | 12 | import pytest 13 | 14 | 15 | @dataclass(frozen=True) 16 | class CodSpeedConfig: 17 | """ 18 | The configuration for the codspeed plugin. 19 | Usually created from the command line arguments. 20 | """ 21 | 22 | warmup_time_ns: int | None = None 23 | max_time_ns: int | None = None 24 | max_rounds: int | None = None 25 | 26 | @classmethod 27 | def from_pytest_config(cls, config: pytest.Config) -> CodSpeedConfig: 28 | warmup_time = config.getoption("--codspeed-warmup-time", None) 29 | warmup_time_ns = ( 30 | int(warmup_time * 1_000_000_000) if warmup_time is not None else None 31 | ) 32 | max_time = config.getoption("--codspeed-max-time", None) 33 | max_time_ns = int(max_time * 1_000_000_000) if max_time is not None else None 34 | return cls( 35 | warmup_time_ns=warmup_time_ns, 36 | max_rounds=config.getoption("--codspeed-max-rounds", None), 37 | max_time_ns=max_time_ns, 38 | ) 39 | 40 | 41 | @dataclass(frozen=True) 42 | class BenchmarkMarkerOptions: 43 | group: str | None = None 44 | """The group name to use for the benchmark.""" 45 | min_time: int | None = None 46 | """ 47 | The minimum time of a round (in seconds). 48 | Only available in walltime mode. 49 | """ 50 | max_time: int | None = None 51 | """ 52 | The maximum time to run the benchmark for (in seconds). 53 | Only available in walltime mode. 54 | """ 55 | max_rounds: int | None = None 56 | """ 57 | The maximum number of rounds to run the benchmark for. 58 | Takes precedence over max_time. Only available in walltime mode. 59 | """ 60 | 61 | @classmethod 62 | def from_pytest_item(cls, item: pytest.Item) -> BenchmarkMarkerOptions: 63 | marker = item.get_closest_marker( 64 | "codspeed_benchmark" 65 | ) or item.get_closest_marker("benchmark") 66 | if marker is None: 67 | return cls() 68 | if len(marker.args) > 0: 69 | raise ValueError( 70 | "Positional arguments are not allowed in the benchmark marker" 71 | ) 72 | kwargs = marker.kwargs 73 | 74 | unknown_kwargs = set(kwargs.keys()) - { 75 | field.name for field in dataclasses.fields(cls) 76 | } 77 | if unknown_kwargs: 78 | raise ValueError( 79 | "Unknown kwargs passed to benchmark marker: " 80 | + ", ".join(sorted(unknown_kwargs)) 81 | ) 82 | 83 | return cls(**kwargs) 84 | 85 | 86 | @dataclass(frozen=True) 87 | class PedanticOptions(Generic[T]): 88 | """Parameters for running a benchmark using the pedantic fixture API.""" 89 | 90 | target: Callable[..., T] 91 | setup: Callable[[], Any | None] | None 92 | teardown: Callable[..., Any | None] | None 93 | rounds: int 94 | warmup_rounds: int 95 | iterations: int 96 | args: tuple[Any, ...] = field(default_factory=tuple) 97 | kwargs: dict[str, Any] = field(default_factory=dict) 98 | 99 | def __post_init__(self) -> None: 100 | if self.rounds < 0: 101 | raise ValueError("rounds must be positive") 102 | if self.warmup_rounds < 0: 103 | raise ValueError("warmup_rounds must be non-negative") 104 | if self.iterations <= 0: 105 | raise ValueError("iterations must be positive") 106 | if self.iterations > 1 and self.setup is not None: 107 | raise ValueError( 108 | "setup cannot be used with multiple iterations, use multiple rounds" 109 | ) 110 | 111 | def setup_and_get_args_kwargs(self) -> tuple[tuple[Any, ...], dict[str, Any]]: 112 | if self.setup is None: 113 | return self.args, self.kwargs 114 | maybe_result = self.setup(*self.args, **self.kwargs) 115 | if maybe_result is not None: 116 | if len(self.args) > 0 or len(self.kwargs) > 0: 117 | raise ValueError("setup cannot return a value when args are provided") 118 | return maybe_result 119 | return self.args, self.kwargs 120 | -------------------------------------------------------------------------------- /src/pytest_codspeed/instruments/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABCMeta, abstractmethod 4 | from enum import Enum 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from typing import Any, Callable, ClassVar, TypeVar 9 | 10 | import pytest 11 | 12 | from pytest_codspeed.config import BenchmarkMarkerOptions, PedanticOptions 13 | from pytest_codspeed.plugin import CodSpeedConfig 14 | 15 | T = TypeVar("T") 16 | 17 | 18 | class Instrument(metaclass=ABCMeta): 19 | instrument: ClassVar[str] 20 | 21 | @abstractmethod 22 | def __init__(self, config: CodSpeedConfig): ... 23 | 24 | @abstractmethod 25 | def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: ... 26 | 27 | @abstractmethod 28 | def measure( 29 | self, 30 | marker_options: BenchmarkMarkerOptions, 31 | name: str, 32 | uri: str, 33 | fn: Callable[..., T], 34 | *args: tuple, 35 | **kwargs: dict[str, Any], 36 | ) -> T: ... 37 | 38 | @abstractmethod 39 | def measure_pedantic( 40 | self, 41 | marker_options: BenchmarkMarkerOptions, 42 | pedantic_options: PedanticOptions[T], 43 | name: str, 44 | uri: str, 45 | ) -> T: ... 46 | 47 | @abstractmethod 48 | def report(self, session: pytest.Session) -> None: ... 49 | 50 | @abstractmethod 51 | def get_result_dict( 52 | self, 53 | ) -> dict[str, Any]: ... 54 | 55 | 56 | class MeasurementMode(str, Enum): 57 | Instrumentation = "instrumentation" 58 | WallTime = "walltime" 59 | 60 | 61 | def get_instrument_from_mode(mode: MeasurementMode) -> type[Instrument]: 62 | from pytest_codspeed.instruments.valgrind import ( 63 | ValgrindInstrument, 64 | ) 65 | from pytest_codspeed.instruments.walltime import WallTimeInstrument 66 | 67 | if mode == MeasurementMode.Instrumentation: 68 | return ValgrindInstrument 69 | else: 70 | return WallTimeInstrument 71 | -------------------------------------------------------------------------------- /src/pytest_codspeed/instruments/hooks/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import sys 5 | import warnings 6 | from typing import TYPE_CHECKING 7 | 8 | from pytest_codspeed.utils import SUPPORTS_PERF_TRAMPOLINE 9 | 10 | if TYPE_CHECKING: 11 | from .dist_instrument_hooks import InstrumentHooksPointer, LibType 12 | 13 | 14 | class InstrumentHooks: 15 | """Zig library wrapper class providing benchmark measurement functionality.""" 16 | 17 | lib: LibType 18 | instance: InstrumentHooksPointer 19 | 20 | def __init__(self) -> None: 21 | if os.environ.get("CODSPEED_ENV") is None: 22 | raise RuntimeError( 23 | "Can't run benchmarks outside of CodSpeed environment." 24 | "Please set the CODSPEED_ENV environment variable." 25 | ) 26 | 27 | try: 28 | from .dist_instrument_hooks import lib # type: ignore 29 | except ImportError as e: 30 | raise RuntimeError(f"Failed to load instrument hooks library: {e}") from e 31 | self.lib = lib 32 | 33 | self.instance = self.lib.instrument_hooks_init() 34 | if self.instance == 0: 35 | raise RuntimeError("Failed to initialize CodSpeed instrumentation library.") 36 | 37 | if SUPPORTS_PERF_TRAMPOLINE: 38 | sys.activate_stack_trampoline("perf") # type: ignore 39 | 40 | def __del__(self): 41 | if hasattr(self, "lib") and hasattr(self, "instance"): 42 | self.lib.instrument_hooks_deinit(self.instance) 43 | 44 | def start_benchmark(self) -> None: 45 | """Start a new benchmark measurement.""" 46 | ret = self.lib.instrument_hooks_start_benchmark(self.instance) 47 | if ret != 0: 48 | warnings.warn("Failed to start benchmark measurement", RuntimeWarning) 49 | 50 | def stop_benchmark(self) -> None: 51 | """Stop the current benchmark measurement.""" 52 | ret = self.lib.instrument_hooks_stop_benchmark(self.instance) 53 | if ret != 0: 54 | warnings.warn("Failed to stop benchmark measurement", RuntimeWarning) 55 | 56 | def set_executed_benchmark(self, uri: str, pid: int | None = None) -> None: 57 | """Set the executed benchmark URI and process ID. 58 | 59 | Args: 60 | uri: The benchmark URI string identifier 61 | pid: Optional process ID (defaults to current process) 62 | """ 63 | if pid is None: 64 | pid = os.getpid() 65 | 66 | ret = self.lib.instrument_hooks_executed_benchmark( 67 | self.instance, pid, uri.encode("ascii") 68 | ) 69 | if ret != 0: 70 | warnings.warn("Failed to set executed benchmark", RuntimeWarning) 71 | 72 | def set_integration(self, name: str, version: str) -> None: 73 | """Set the integration name and version.""" 74 | ret = self.lib.instrument_hooks_set_integration( 75 | self.instance, name.encode("ascii"), version.encode("ascii") 76 | ) 77 | if ret != 0: 78 | warnings.warn("Failed to set integration name and version", RuntimeWarning) 79 | 80 | def is_instrumented(self) -> bool: 81 | """Check if instrumentation is active.""" 82 | return self.lib.instrument_hooks_is_instrumented(self.instance) 83 | -------------------------------------------------------------------------------- /src/pytest_codspeed/instruments/hooks/build.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from cffi import FFI # type: ignore 4 | 5 | ffibuilder = FFI() 6 | 7 | includes_dir = Path(__file__).parent.joinpath("instrument-hooks/includes") 8 | header_text = (includes_dir / "core.h").read_text() 9 | filtered_header = "\n".join( 10 | line for line in header_text.splitlines() if not line.strip().startswith("#") 11 | ) 12 | ffibuilder.cdef(filtered_header) 13 | 14 | ffibuilder.set_source( 15 | "pytest_codspeed.instruments.hooks.dist_instrument_hooks", 16 | """ 17 | #include "core.h" 18 | """, 19 | sources=[ 20 | "src/pytest_codspeed/instruments/hooks/instrument-hooks/dist/core.c", 21 | ], 22 | include_dirs=[str(includes_dir)], 23 | ) 24 | 25 | if __name__ == "__main__": 26 | ffibuilder.compile(verbose=True) 27 | -------------------------------------------------------------------------------- /src/pytest_codspeed/instruments/hooks/dist_instrument_hooks.pyi: -------------------------------------------------------------------------------- 1 | InstrumentHooksPointer = object 2 | 3 | class lib: 4 | @staticmethod 5 | def instrument_hooks_init() -> InstrumentHooksPointer: ... 6 | @staticmethod 7 | def instrument_hooks_deinit(hooks: InstrumentHooksPointer) -> None: ... 8 | @staticmethod 9 | def instrument_hooks_is_instrumented(hooks: InstrumentHooksPointer) -> bool: ... 10 | @staticmethod 11 | def instrument_hooks_start_benchmark(hooks: InstrumentHooksPointer) -> int: ... 12 | @staticmethod 13 | def instrument_hooks_stop_benchmark(hooks: InstrumentHooksPointer) -> int: ... 14 | @staticmethod 15 | def instrument_hooks_executed_benchmark( 16 | hooks: InstrumentHooksPointer, pid: int, uri: bytes 17 | ) -> int: ... 18 | @staticmethod 19 | def instrument_hooks_set_integration( 20 | hooks: InstrumentHooksPointer, name: bytes, version: bytes 21 | ) -> int: ... 22 | @staticmethod 23 | def callgrind_start_instrumentation() -> int: ... 24 | @staticmethod 25 | def callgrind_stop_instrumentation() -> int: ... 26 | 27 | LibType = type[lib] 28 | -------------------------------------------------------------------------------- /src/pytest_codspeed/instruments/valgrind.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | from typing import TYPE_CHECKING 5 | 6 | from pytest_codspeed import __semver_version__ 7 | from pytest_codspeed.instruments import Instrument 8 | from pytest_codspeed.instruments.hooks import InstrumentHooks 9 | from pytest_codspeed.utils import SUPPORTS_PERF_TRAMPOLINE 10 | 11 | if TYPE_CHECKING: 12 | from typing import Any, Callable 13 | 14 | from pytest import Session 15 | 16 | from pytest_codspeed.config import PedanticOptions 17 | from pytest_codspeed.instruments import T 18 | from pytest_codspeed.plugin import BenchmarkMarkerOptions, CodSpeedConfig 19 | 20 | 21 | class ValgrindInstrument(Instrument): 22 | instrument = "valgrind" 23 | instrument_hooks: InstrumentHooks | None 24 | 25 | def __init__(self, config: CodSpeedConfig) -> None: 26 | self.benchmark_count = 0 27 | try: 28 | self.instrument_hooks = InstrumentHooks() 29 | self.instrument_hooks.set_integration("pytest-codspeed", __semver_version__) 30 | except RuntimeError: 31 | self.instrument_hooks = None 32 | 33 | self.should_measure = self.instrument_hooks is not None 34 | 35 | def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: 36 | config = ( 37 | f"mode: instrumentation, " 38 | f"callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'}" 39 | ) 40 | warnings = [] 41 | if not self.should_measure: 42 | warnings.append( 43 | "\033[1m" 44 | "NOTICE: codspeed is enabled, but no performance measurement" 45 | " will be made since it's running in an unknown environment." 46 | "\033[0m" 47 | ) 48 | return config, warnings 49 | 50 | def measure( 51 | self, 52 | marker_options: BenchmarkMarkerOptions, 53 | name: str, 54 | uri: str, 55 | fn: Callable[..., T], 56 | *args: tuple, 57 | **kwargs: dict[str, Any], 58 | ) -> T: 59 | self.benchmark_count += 1 60 | 61 | if not self.instrument_hooks: 62 | return fn(*args, **kwargs) 63 | 64 | def __codspeed_root_frame__() -> T: 65 | return fn(*args, **kwargs) 66 | 67 | if SUPPORTS_PERF_TRAMPOLINE: 68 | # Warmup CPython performance map cache 69 | __codspeed_root_frame__() 70 | 71 | # Manually call the library function to avoid an extra stack frame. Also 72 | # call the callgrind markers directly to avoid extra overhead. 73 | self.instrument_hooks.lib.callgrind_start_instrumentation() 74 | try: 75 | return __codspeed_root_frame__() 76 | finally: 77 | # Ensure instrumentation is stopped even if the test failed 78 | self.instrument_hooks.lib.callgrind_stop_instrumentation() 79 | self.instrument_hooks.set_executed_benchmark(uri) 80 | 81 | def measure_pedantic( 82 | self, 83 | marker_options: BenchmarkMarkerOptions, 84 | pedantic_options: PedanticOptions[T], 85 | name: str, 86 | uri: str, 87 | ) -> T: 88 | if pedantic_options.rounds != 1 or pedantic_options.iterations != 1: 89 | warnings.warn( 90 | "Valgrind instrument ignores rounds and iterations settings " 91 | "in pedantic mode" 92 | ) 93 | if not self.instrument_hooks: 94 | args, kwargs = pedantic_options.setup_and_get_args_kwargs() 95 | out = pedantic_options.target(*args, **kwargs) 96 | if pedantic_options.teardown is not None: 97 | pedantic_options.teardown(*args, **kwargs) 98 | return out 99 | 100 | def __codspeed_root_frame__(*args, **kwargs) -> T: 101 | return pedantic_options.target(*args, **kwargs) 102 | 103 | # Warmup 104 | warmup_rounds = max( 105 | pedantic_options.warmup_rounds, 1 if SUPPORTS_PERF_TRAMPOLINE else 0 106 | ) 107 | for _ in range(warmup_rounds): 108 | args, kwargs = pedantic_options.setup_and_get_args_kwargs() 109 | __codspeed_root_frame__(*args, **kwargs) 110 | if pedantic_options.teardown is not None: 111 | pedantic_options.teardown(*args, **kwargs) 112 | 113 | # Compute the actual result of the function 114 | args, kwargs = pedantic_options.setup_and_get_args_kwargs() 115 | self.instrument_hooks.lib.callgrind_start_instrumentation() 116 | try: 117 | out = __codspeed_root_frame__(*args, **kwargs) 118 | finally: 119 | self.instrument_hooks.lib.callgrind_stop_instrumentation() 120 | self.instrument_hooks.set_executed_benchmark(uri) 121 | if pedantic_options.teardown is not None: 122 | pedantic_options.teardown(*args, **kwargs) 123 | 124 | return out 125 | 126 | def report(self, session: Session) -> None: 127 | reporter = session.config.pluginmanager.get_plugin("terminalreporter") 128 | assert reporter is not None, "terminalreporter not found" 129 | count_suffix = "benchmarked" if self.should_measure else "benchmark tested" 130 | reporter.write_sep( 131 | "=", 132 | f"{self.benchmark_count} {count_suffix}", 133 | ) 134 | 135 | def get_result_dict(self) -> dict[str, Any]: 136 | return { 137 | "instrument": {"type": self.instrument}, 138 | # bench results will be dumped by valgrind 139 | } 140 | -------------------------------------------------------------------------------- /src/pytest_codspeed/instruments/walltime.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import warnings 5 | from dataclasses import asdict, dataclass 6 | from math import ceil 7 | from statistics import mean, quantiles, stdev 8 | from time import get_clock_info, perf_counter_ns 9 | from typing import TYPE_CHECKING 10 | 11 | from rich.console import Console 12 | from rich.markup import escape 13 | from rich.table import Table 14 | from rich.text import Text 15 | 16 | from pytest_codspeed import __semver_version__ 17 | from pytest_codspeed.instruments import Instrument 18 | from pytest_codspeed.instruments.hooks import InstrumentHooks 19 | from pytest_codspeed.utils import SUPPORTS_PERF_TRAMPOLINE 20 | 21 | if TYPE_CHECKING: 22 | from typing import Any, Callable 23 | 24 | from pytest import Session 25 | 26 | from pytest_codspeed.config import PedanticOptions 27 | from pytest_codspeed.instruments import T 28 | from pytest_codspeed.plugin import BenchmarkMarkerOptions, CodSpeedConfig 29 | 30 | DEFAULT_WARMUP_TIME_NS = 1_000_000_000 31 | DEFAULT_MAX_TIME_NS = 3_000_000_000 32 | TIMER_RESOLUTION_NS = get_clock_info("perf_counter").resolution * 1e9 33 | DEFAULT_MIN_ROUND_TIME_NS = int(TIMER_RESOLUTION_NS * 1_000_000) 34 | 35 | IQR_OUTLIER_FACTOR = 1.5 36 | STDEV_OUTLIER_FACTOR = 3 37 | 38 | 39 | @dataclass 40 | class BenchmarkConfig: 41 | warmup_time_ns: int 42 | min_round_time_ns: float 43 | max_time_ns: int 44 | max_rounds: int | None 45 | 46 | @classmethod 47 | def from_codspeed_config_and_marker_data( 48 | cls, config: CodSpeedConfig, marker_data: BenchmarkMarkerOptions 49 | ) -> BenchmarkConfig: 50 | if marker_data.max_time is not None: 51 | max_time_ns = int(marker_data.max_time * 1e9) 52 | elif config.max_time_ns is not None: 53 | max_time_ns = config.max_time_ns 54 | else: 55 | max_time_ns = DEFAULT_MAX_TIME_NS 56 | 57 | if marker_data.max_rounds is not None: 58 | max_rounds = marker_data.max_rounds 59 | elif config.max_rounds is not None: 60 | max_rounds = config.max_rounds 61 | else: 62 | max_rounds = None 63 | 64 | if marker_data.min_time is not None: 65 | min_round_time_ns = int(marker_data.min_time * 1e9) 66 | else: 67 | min_round_time_ns = DEFAULT_MIN_ROUND_TIME_NS 68 | 69 | return cls( 70 | warmup_time_ns=config.warmup_time_ns 71 | if config.warmup_time_ns is not None 72 | else DEFAULT_WARMUP_TIME_NS, 73 | min_round_time_ns=min_round_time_ns, 74 | max_time_ns=max_time_ns, 75 | max_rounds=max_rounds, 76 | ) 77 | 78 | 79 | @dataclass 80 | class BenchmarkStats: 81 | min_ns: float 82 | max_ns: float 83 | mean_ns: float 84 | stdev_ns: float 85 | 86 | q1_ns: float 87 | median_ns: float 88 | q3_ns: float 89 | 90 | rounds: int 91 | total_time: float 92 | iqr_outlier_rounds: int 93 | stdev_outlier_rounds: int 94 | iter_per_round: int 95 | warmup_iters: int 96 | 97 | @classmethod 98 | def from_list( 99 | cls, 100 | times_per_round_ns: list[float], 101 | *, 102 | rounds: int, 103 | iter_per_round: int, 104 | warmup_iters: int, 105 | total_time: float, 106 | ) -> BenchmarkStats: 107 | times_ns = [t / iter_per_round for t in times_per_round_ns] 108 | stdev_ns = stdev(times_ns) if len(times_ns) > 1 else 0 109 | mean_ns = mean(times_ns) 110 | if len(times_ns) > 1: 111 | q1_ns, median_ns, q3_ns = quantiles(times_ns, n=4) 112 | else: 113 | q1_ns, median_ns, q3_ns = ( 114 | mean_ns, 115 | mean_ns, 116 | mean_ns, 117 | ) 118 | iqr_ns = q3_ns - q1_ns 119 | iqr_outlier_rounds = sum( 120 | 1 121 | for t in times_ns 122 | if t < q1_ns - IQR_OUTLIER_FACTOR * iqr_ns 123 | or t > q3_ns + IQR_OUTLIER_FACTOR * iqr_ns 124 | ) 125 | stdev_outlier_rounds = sum( 126 | 1 127 | for t in times_ns 128 | if t < mean_ns - STDEV_OUTLIER_FACTOR * stdev_ns 129 | or t > mean_ns + STDEV_OUTLIER_FACTOR * stdev_ns 130 | ) 131 | 132 | return cls( 133 | min_ns=min(times_ns), 134 | max_ns=max(times_ns), 135 | stdev_ns=stdev_ns, 136 | mean_ns=mean_ns, 137 | q1_ns=q1_ns, 138 | median_ns=median_ns, 139 | q3_ns=q3_ns, 140 | rounds=rounds, 141 | total_time=total_time, 142 | iqr_outlier_rounds=iqr_outlier_rounds, 143 | stdev_outlier_rounds=stdev_outlier_rounds, 144 | iter_per_round=iter_per_round, 145 | warmup_iters=warmup_iters, 146 | ) 147 | 148 | 149 | @dataclass 150 | class Benchmark: 151 | name: str 152 | uri: str 153 | 154 | config: BenchmarkConfig 155 | stats: BenchmarkStats 156 | 157 | 158 | class WallTimeInstrument(Instrument): 159 | instrument = "walltime" 160 | instrument_hooks: InstrumentHooks | None 161 | 162 | def __init__(self, config: CodSpeedConfig) -> None: 163 | try: 164 | self.instrument_hooks = InstrumentHooks() 165 | self.instrument_hooks.set_integration("pytest-codspeed", __semver_version__) 166 | except RuntimeError as e: 167 | if os.environ.get("CODSPEED_ENV") is not None: 168 | warnings.warn( 169 | f"Failed to initialize instrument hooks: {e}", RuntimeWarning 170 | ) 171 | self.instrument_hooks = None 172 | 173 | self.config = config 174 | self.benchmarks: list[Benchmark] = [] 175 | 176 | def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: 177 | config_str = ( 178 | f"mode: walltime, " 179 | f"callgraph: " 180 | f"{'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'}, " 181 | f"timer_resolution: {TIMER_RESOLUTION_NS:.1f}ns" 182 | ) 183 | return config_str, [] 184 | 185 | def measure( 186 | self, 187 | marker_options: BenchmarkMarkerOptions, 188 | name: str, 189 | uri: str, 190 | fn: Callable[..., T], 191 | *args: tuple, 192 | **kwargs: dict[str, Any], 193 | ) -> T: 194 | benchmark_config = BenchmarkConfig.from_codspeed_config_and_marker_data( 195 | self.config, marker_options 196 | ) 197 | 198 | def __codspeed_root_frame__() -> T: 199 | return fn(*args, **kwargs) 200 | 201 | # Compute the actual result of the function 202 | out = __codspeed_root_frame__() 203 | 204 | # Warmup 205 | times_per_round_ns: list[float] = [] 206 | warmup_start = start = perf_counter_ns() 207 | while True: 208 | start = perf_counter_ns() 209 | __codspeed_root_frame__() 210 | end = perf_counter_ns() 211 | times_per_round_ns.append(end - start) 212 | if end - warmup_start > benchmark_config.warmup_time_ns: 213 | break 214 | 215 | # Round sizing 216 | warmup_mean_ns = mean(times_per_round_ns) 217 | warmup_iters = len(times_per_round_ns) 218 | times_per_round_ns.clear() 219 | iter_per_round = ( 220 | int(ceil(benchmark_config.min_round_time_ns / warmup_mean_ns)) 221 | if warmup_mean_ns <= benchmark_config.min_round_time_ns 222 | else 1 223 | ) 224 | if benchmark_config.max_rounds is None: 225 | round_time_ns = warmup_mean_ns * iter_per_round 226 | rounds = int(benchmark_config.max_time_ns / round_time_ns) 227 | else: 228 | rounds = benchmark_config.max_rounds 229 | rounds = max(1, rounds) 230 | 231 | # Benchmark 232 | iter_range = range(iter_per_round) 233 | run_start = perf_counter_ns() 234 | if self.instrument_hooks: 235 | self.instrument_hooks.start_benchmark() 236 | for _ in range(rounds): 237 | start = perf_counter_ns() 238 | for _ in iter_range: 239 | __codspeed_root_frame__() 240 | end = perf_counter_ns() 241 | times_per_round_ns.append(end - start) 242 | 243 | if end - run_start > benchmark_config.max_time_ns: 244 | # TODO: log something 245 | break 246 | if self.instrument_hooks: 247 | self.instrument_hooks.stop_benchmark() 248 | self.instrument_hooks.set_executed_benchmark(uri) 249 | benchmark_end = perf_counter_ns() 250 | total_time = (benchmark_end - run_start) / 1e9 251 | 252 | stats = BenchmarkStats.from_list( 253 | times_per_round_ns, 254 | rounds=rounds, 255 | total_time=total_time, 256 | iter_per_round=iter_per_round, 257 | warmup_iters=warmup_iters, 258 | ) 259 | 260 | self.benchmarks.append( 261 | Benchmark(name=name, uri=uri, config=benchmark_config, stats=stats) 262 | ) 263 | return out 264 | 265 | def measure_pedantic( # noqa: C901 266 | self, 267 | marker_options: BenchmarkMarkerOptions, 268 | pedantic_options: PedanticOptions[T], 269 | name: str, 270 | uri: str, 271 | ) -> T: 272 | benchmark_config = BenchmarkConfig.from_codspeed_config_and_marker_data( 273 | self.config, marker_options 274 | ) 275 | 276 | def __codspeed_root_frame__(*args, **kwargs) -> T: 277 | return pedantic_options.target(*args, **kwargs) 278 | 279 | iter_range = range(pedantic_options.iterations) 280 | 281 | # Warmup 282 | for _ in range(pedantic_options.warmup_rounds): 283 | args, kwargs = pedantic_options.setup_and_get_args_kwargs() 284 | for _ in iter_range: 285 | __codspeed_root_frame__(*args, **kwargs) 286 | if pedantic_options.teardown is not None: 287 | pedantic_options.teardown(*args, **kwargs) 288 | 289 | # Benchmark 290 | times_per_round_ns: list[float] = [] 291 | benchmark_start = perf_counter_ns() 292 | if self.instrument_hooks: 293 | self.instrument_hooks.start_benchmark() 294 | for _ in range(pedantic_options.rounds): 295 | start = perf_counter_ns() 296 | args, kwargs = pedantic_options.setup_and_get_args_kwargs() 297 | for _ in iter_range: 298 | __codspeed_root_frame__(*args, **kwargs) 299 | end = perf_counter_ns() 300 | times_per_round_ns.append(end - start) 301 | if pedantic_options.teardown is not None: 302 | pedantic_options.teardown(*args, **kwargs) 303 | if self.instrument_hooks: 304 | self.instrument_hooks.stop_benchmark() 305 | self.instrument_hooks.set_executed_benchmark(uri) 306 | benchmark_end = perf_counter_ns() 307 | total_time = (benchmark_end - benchmark_start) / 1e9 308 | stats = BenchmarkStats.from_list( 309 | times_per_round_ns, 310 | rounds=pedantic_options.rounds, 311 | total_time=total_time, 312 | iter_per_round=pedantic_options.iterations, 313 | warmup_iters=pedantic_options.warmup_rounds, 314 | ) 315 | 316 | # Compute the actual result of the function 317 | args, kwargs = pedantic_options.setup_and_get_args_kwargs() 318 | out = __codspeed_root_frame__(*args, **kwargs) 319 | if pedantic_options.teardown is not None: 320 | pedantic_options.teardown(*args, **kwargs) 321 | 322 | self.benchmarks.append( 323 | Benchmark(name=name, uri=uri, config=benchmark_config, stats=stats) 324 | ) 325 | return out 326 | 327 | def report(self, session: Session) -> None: 328 | reporter = session.config.pluginmanager.get_plugin("terminalreporter") 329 | assert reporter is not None, "terminalreporter not found" 330 | 331 | if len(self.benchmarks) == 0: 332 | reporter.write_sep( 333 | "=", 334 | f"{len(self.benchmarks)} benchmarked", 335 | ) 336 | return 337 | self._print_benchmark_table() 338 | reporter.write_sep( 339 | "=", 340 | f"{len(self.benchmarks)} benchmarked", 341 | ) 342 | 343 | def _print_benchmark_table(self) -> None: 344 | table = Table(title="Benchmark Results") 345 | 346 | table.add_column("Benchmark", justify="right", style="cyan", no_wrap=True) 347 | table.add_column("Time (best)", justify="right", style="green bold") 348 | table.add_column( 349 | "Rel. StdDev", 350 | justify="right", 351 | ) 352 | table.add_column("Run time", justify="right") 353 | table.add_column("Iters", justify="right") 354 | 355 | for bench in self.benchmarks: 356 | rsd = bench.stats.stdev_ns / bench.stats.mean_ns 357 | rsd_text = Text(f"{rsd * 100:.1f}%") 358 | if rsd > 0.1: 359 | rsd_text.stylize("red bold") 360 | table.add_row( 361 | escape(bench.name), 362 | f"{bench.stats.min_ns / bench.stats.iter_per_round:,.0f}ns", 363 | rsd_text, 364 | f"{bench.stats.total_time:,.2f}s", 365 | f"{bench.stats.iter_per_round * bench.stats.rounds:,}", 366 | ) 367 | 368 | console = Console() 369 | print("\n") 370 | console.print(table) 371 | 372 | def get_result_dict(self) -> dict[str, Any]: 373 | return { 374 | "instrument": { 375 | "type": self.instrument, 376 | "clock_info": get_clock_info("perf_counter").__dict__, 377 | }, 378 | "benchmarks": [asdict(bench) for bench in self.benchmarks], 379 | } 380 | -------------------------------------------------------------------------------- /src/pytest_codspeed/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | import gc 5 | import json 6 | import os 7 | import random 8 | from dataclasses import dataclass, field 9 | from pathlib import Path 10 | from time import time 11 | from typing import TYPE_CHECKING 12 | 13 | import pytest 14 | from _pytest.fixtures import FixtureManager 15 | 16 | from pytest_codspeed.config import ( 17 | BenchmarkMarkerOptions, 18 | CodSpeedConfig, 19 | PedanticOptions, 20 | ) 21 | from pytest_codspeed.instruments import MeasurementMode, get_instrument_from_mode 22 | from pytest_codspeed.utils import ( 23 | BEFORE_PYTEST_8_1_1, 24 | IS_PYTEST_BENCHMARK_INSTALLED, 25 | IS_PYTEST_SPEED_INSTALLED, 26 | get_environment_metadata, 27 | get_git_relative_uri_and_name, 28 | ) 29 | 30 | from . import __version__ 31 | 32 | if TYPE_CHECKING: 33 | from typing import Any, Callable, TypeVar 34 | 35 | from pytest_codspeed.instruments import Instrument 36 | 37 | T = TypeVar("T") 38 | 39 | 40 | @pytest.hookimpl(trylast=True) 41 | def pytest_addoption(parser: pytest.Parser): 42 | group = parser.getgroup("CodSpeed benchmarking") 43 | group.addoption( 44 | "--codspeed", 45 | action="store_true", 46 | default=False, 47 | help="Enable codspeed (not required when using the CodSpeed action)", 48 | ) 49 | group.addoption( 50 | "--codspeed-mode", 51 | action="store", 52 | choices=[mode.value for mode in MeasurementMode], 53 | help="The measurement tool to use for measuring performance", 54 | ) 55 | group.addoption( 56 | "--codspeed-warmup-time", 57 | action="store", 58 | type=float, 59 | help=( 60 | "The time to warm up the benchmark for (in seconds), only for walltime mode" 61 | ), 62 | ) 63 | group.addoption( 64 | "--codspeed-max-time", 65 | action="store", 66 | type=float, 67 | help=( 68 | "The maximum time to run a benchmark for (in seconds), " 69 | "only for walltime mode" 70 | ), 71 | ) 72 | group.addoption( 73 | "--codspeed-max-rounds", 74 | action="store", 75 | type=int, 76 | help=( 77 | "The maximum number of rounds to run a benchmark for" 78 | ", only for walltime mode" 79 | ), 80 | ) 81 | 82 | 83 | @dataclass(unsafe_hash=True) 84 | class CodSpeedPlugin: 85 | is_codspeed_enabled: bool 86 | mode: MeasurementMode 87 | instrument: Instrument 88 | config: CodSpeedConfig 89 | disabled_plugins: tuple[str, ...] 90 | profile_folder: Path | None 91 | benchmark_count: int = field(default=0, hash=False, compare=False) 92 | 93 | 94 | PLUGIN_NAME = "codspeed_plugin" 95 | 96 | 97 | def get_plugin(config: pytest.Config) -> CodSpeedPlugin: 98 | return config.pluginmanager.get_plugin(PLUGIN_NAME) 99 | 100 | 101 | @pytest.hookimpl(tryfirst=True) 102 | def pytest_configure(config: pytest.Config): 103 | config.addinivalue_line( 104 | "markers", "codspeed_benchmark: mark an entire test for codspeed benchmarking" 105 | ) 106 | config.addinivalue_line( 107 | "markers", "benchmark: mark an entire test for codspeed benchmarking" 108 | ) 109 | is_codspeed_enabled = ( 110 | config.getoption("--codspeed") or os.environ.get("CODSPEED_ENV") is not None 111 | ) 112 | 113 | if os.environ.get("CODSPEED_ENV") is not None: 114 | if os.environ.get("CODSPEED_RUNNER_MODE") == "walltime": 115 | default_mode = MeasurementMode.WallTime.value 116 | else: 117 | default_mode = MeasurementMode.Instrumentation.value 118 | else: 119 | default_mode = MeasurementMode.WallTime.value 120 | 121 | mode = MeasurementMode(config.getoption("--codspeed-mode", None) or default_mode) 122 | instrument = get_instrument_from_mode(mode) 123 | disabled_plugins: list[str] = [] 124 | if is_codspeed_enabled: 125 | if IS_PYTEST_BENCHMARK_INSTALLED: 126 | # Disable pytest-benchmark 127 | object.__setattr__(config.option, "benchmark_disable", True) 128 | config.pluginmanager.set_blocked("pytest_benchmark") 129 | config.pluginmanager.set_blocked("pytest-benchmark") 130 | disabled_plugins.append("pytest-benchmark") 131 | if IS_PYTEST_SPEED_INSTALLED: 132 | # Disable pytest-speed 133 | config.pluginmanager.set_blocked("speed") 134 | disabled_plugins.append("pytest-speed") 135 | 136 | profile_folder = os.environ.get("CODSPEED_PROFILE_FOLDER") 137 | 138 | codspeed_config = CodSpeedConfig.from_pytest_config(config) 139 | 140 | plugin = CodSpeedPlugin( 141 | disabled_plugins=tuple(disabled_plugins), 142 | is_codspeed_enabled=is_codspeed_enabled, 143 | mode=mode, 144 | instrument=instrument(codspeed_config), 145 | config=codspeed_config, 146 | profile_folder=Path(profile_folder) if profile_folder else None, 147 | ) 148 | config.pluginmanager.register(plugin, PLUGIN_NAME) 149 | 150 | 151 | @pytest.hookimpl() 152 | def pytest_plugin_registered(plugin, manager: pytest.PytestPluginManager): 153 | """ 154 | Patch the benchmark fixture to use the codspeed one if codspeed is enabled and an 155 | alternative benchmark fixture is available 156 | """ 157 | if (IS_PYTEST_BENCHMARK_INSTALLED or IS_PYTEST_SPEED_INSTALLED) and isinstance( 158 | plugin, FixtureManager 159 | ): 160 | fixture_manager = plugin 161 | codspeed_plugin: CodSpeedPlugin = manager.get_plugin(PLUGIN_NAME) 162 | if codspeed_plugin.is_codspeed_enabled: 163 | codspeed_benchmark_fixtures = plugin.getfixturedefs( 164 | "codspeed_benchmark", 165 | fixture_manager.session.nodeid 166 | if BEFORE_PYTEST_8_1_1 167 | else fixture_manager.session, 168 | ) 169 | assert codspeed_benchmark_fixtures is not None 170 | # Archive the alternative benchmark fixture 171 | fixture_manager._arg2fixturedefs["__benchmark"] = ( 172 | fixture_manager._arg2fixturedefs["benchmark"] 173 | ) 174 | # Replace the alternative fixture with the codspeed one 175 | fixture_manager._arg2fixturedefs["benchmark"] = codspeed_benchmark_fixtures 176 | 177 | 178 | @pytest.hookimpl(trylast=True) 179 | def pytest_report_header(config: pytest.Config): 180 | plugin = get_plugin(config) 181 | config_str, warns = plugin.instrument.get_instrument_config_str_and_warns() 182 | out = [ 183 | ( 184 | f"codspeed: {__version__} (" 185 | f"{'enabled' if plugin.is_codspeed_enabled else 'disabled'}, {config_str}" 186 | ")" 187 | ), 188 | *warns, 189 | ] 190 | if len(plugin.disabled_plugins) > 0: 191 | out.append( 192 | "\033[93mCodSpeed had to disable the following plugins: " 193 | f"{', '.join(plugin.disabled_plugins)}\033[0m" 194 | ) 195 | return "\n".join(out) 196 | 197 | 198 | def has_benchmark_fixture(item: pytest.Item) -> bool: 199 | item_fixtures = getattr(item, "fixturenames", []) 200 | return "benchmark" in item_fixtures or "codspeed_benchmark" in item_fixtures 201 | 202 | 203 | def has_benchmark_marker(item: pytest.Item) -> bool: 204 | return ( 205 | item.get_closest_marker("codspeed_benchmark") is not None 206 | or item.get_closest_marker("benchmark") is not None 207 | ) 208 | 209 | 210 | def should_benchmark_item(item: pytest.Item) -> bool: 211 | return has_benchmark_fixture(item) or has_benchmark_marker(item) 212 | 213 | 214 | @pytest.hookimpl(trylast=True) 215 | def pytest_collection_modifyitems( 216 | session: pytest.Session, config: pytest.Config, items: list[pytest.Item] 217 | ): 218 | """Filter out items that should not be benchmarked when codspeed is enabled""" 219 | plugin = get_plugin(config) 220 | if plugin.is_codspeed_enabled: 221 | deselected = [] 222 | selected = [] 223 | for item in items: 224 | if should_benchmark_item(item): 225 | selected.append(item) 226 | else: 227 | deselected.append(item) 228 | config.hook.pytest_deselected(items=deselected) 229 | items[:] = selected 230 | 231 | 232 | def _measure( 233 | plugin: CodSpeedPlugin, 234 | node: pytest.Item, 235 | config: pytest.Config, 236 | pedantic_options: PedanticOptions | None, 237 | fn: Callable[..., T], 238 | args: tuple[Any, ...], 239 | kwargs: dict[str, Any], 240 | ) -> T: 241 | marker_options = BenchmarkMarkerOptions.from_pytest_item(node) 242 | random.seed(0) 243 | is_gc_enabled = gc.isenabled() 244 | if is_gc_enabled: 245 | gc.collect() 246 | gc.disable() 247 | try: 248 | uri, name = get_git_relative_uri_and_name(node.nodeid, config.rootpath) 249 | if pedantic_options is None: 250 | return plugin.instrument.measure( 251 | marker_options, name, uri, fn, *args, **kwargs 252 | ) 253 | else: 254 | return plugin.instrument.measure_pedantic( 255 | marker_options, pedantic_options, name, uri 256 | ) 257 | finally: 258 | # Ensure GC is re-enabled even if the test failed 259 | if is_gc_enabled: 260 | gc.enable() 261 | 262 | 263 | def wrap_runtest( 264 | plugin: CodSpeedPlugin, 265 | node: pytest.Item, 266 | config: pytest.Config, 267 | fn: Callable[..., T], 268 | ) -> Callable[..., T]: 269 | @functools.wraps(fn) 270 | def wrapped(*args: tuple, **kwargs: dict[str, Any]) -> T: 271 | return _measure(plugin, node, config, None, fn, args, kwargs) 272 | 273 | return wrapped 274 | 275 | 276 | @pytest.hookimpl(tryfirst=True) 277 | def pytest_runtest_protocol(item: pytest.Item, nextitem: pytest.Item | None): 278 | plugin = get_plugin(item.config) 279 | if not plugin.is_codspeed_enabled or not should_benchmark_item(item): 280 | # Defer to the default test protocol since no benchmarking is needed 281 | return None 282 | 283 | if has_benchmark_fixture(item): 284 | # Instrumentation is handled by the fixture 285 | return None 286 | 287 | # Wrap runtest and defer to default protocol 288 | item.runtest = wrap_runtest(plugin, item, item.config, item.runtest) 289 | return None 290 | 291 | 292 | @pytest.hookimpl() 293 | def pytest_sessionfinish(session: pytest.Session, exitstatus): 294 | plugin = get_plugin(session.config) 295 | if plugin.is_codspeed_enabled: 296 | plugin.instrument.report(session) 297 | if plugin.profile_folder: 298 | result_path = plugin.profile_folder / "results" / f"{os.getpid()}.json" 299 | else: 300 | result_path = ( 301 | session.config.rootpath / f".codspeed/results_{time() * 1000:.0f}.json" 302 | ) 303 | data = {**get_environment_metadata(), **plugin.instrument.get_result_dict()} 304 | result_path.parent.mkdir(parents=True, exist_ok=True) 305 | result_path.write_text(json.dumps(data, indent=2)) 306 | 307 | 308 | class BenchmarkFixture: 309 | """The fixture that can be used to benchmark a function.""" 310 | 311 | @property # type: ignore 312 | def __class__(self): 313 | # Bypass the pytest-benchmark fixture class check 314 | # https://github.com/ionelmc/pytest-benchmark/commit/d6511e3474931feb4e862948128e0c389acfceec 315 | if IS_PYTEST_BENCHMARK_INSTALLED: 316 | from pytest_benchmark.fixture import ( 317 | BenchmarkFixture as PytestBenchmarkFixture, 318 | ) 319 | 320 | return PytestBenchmarkFixture 321 | return BenchmarkFixture 322 | 323 | def __init__(self, request: pytest.FixtureRequest): 324 | self.extra_info: dict = {} 325 | 326 | self._request = request 327 | self._config = self._request.config 328 | self._plugin = get_plugin(self._config) 329 | self._called = False 330 | 331 | def __call__( 332 | self, target: Callable[..., T], *args: tuple, **kwargs: dict[str, Any] 333 | ) -> T: 334 | if self._called: 335 | raise RuntimeError("The benchmark fixture can only be used once per test") 336 | self._called = True 337 | if self._plugin.is_codspeed_enabled: 338 | return _measure( 339 | self._plugin, 340 | self._request.node, 341 | self._config, 342 | None, 343 | target, 344 | args, 345 | kwargs, 346 | ) 347 | else: 348 | return target(*args, **kwargs) 349 | 350 | def pedantic( 351 | self, 352 | target: Callable[..., T], 353 | args: tuple[Any, ...] = (), 354 | kwargs: dict[str, Any] = {}, 355 | setup: Callable | None = None, 356 | teardown: Callable | None = None, 357 | rounds: int = 1, 358 | warmup_rounds: int = 0, 359 | iterations: int = 1, 360 | ): 361 | if self._called: 362 | raise RuntimeError("The benchmark fixture can only be used once per test") 363 | self._called = True 364 | pedantic_options = PedanticOptions( 365 | target=target, 366 | args=args, 367 | kwargs=kwargs, 368 | setup=setup, 369 | teardown=teardown, 370 | rounds=rounds, 371 | warmup_rounds=warmup_rounds, 372 | iterations=iterations, 373 | ) 374 | if self._plugin.is_codspeed_enabled: 375 | return _measure( 376 | self._plugin, 377 | self._request.node, 378 | self._config, 379 | pedantic_options, 380 | target, 381 | args, 382 | kwargs, 383 | ) 384 | else: 385 | args, kwargs = pedantic_options.setup_and_get_args_kwargs() 386 | result = target(*args, **kwargs) 387 | if pedantic_options.teardown is not None: 388 | pedantic_options.teardown(*args, **kwargs) 389 | return result 390 | 391 | 392 | @pytest.fixture(scope="function") 393 | def codspeed_benchmark(request: pytest.FixtureRequest) -> Callable: 394 | return BenchmarkFixture(request) 395 | 396 | 397 | if not IS_PYTEST_BENCHMARK_INSTALLED: 398 | 399 | @pytest.fixture(scope="function") 400 | def benchmark(codspeed_benchmark, request: pytest.FixtureRequest): 401 | """ 402 | Compatibility with pytest-benchmark 403 | """ 404 | return codspeed_benchmark 405 | -------------------------------------------------------------------------------- /src/pytest_codspeed/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodSpeedHQ/pytest-codspeed/3e26619fa5d36309c4fa9a6cb9176cadf1381989/src/pytest_codspeed/py.typed -------------------------------------------------------------------------------- /src/pytest_codspeed/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib.util 4 | import os 5 | import sys 6 | import sysconfig 7 | from pathlib import Path 8 | 9 | import pytest 10 | 11 | from pytest_codspeed import __semver_version__ 12 | 13 | if sys.version_info < (3, 10): 14 | import importlib_metadata as importlib_metadata 15 | else: 16 | import importlib.metadata as importlib_metadata 17 | 18 | 19 | IS_PYTEST_BENCHMARK_INSTALLED = importlib.util.find_spec("pytest_benchmark") is not None 20 | IS_PYTEST_SPEED_INSTALLED = importlib.util.find_spec("pytest_speed") is not None 21 | BEFORE_PYTEST_8_1_1 = pytest.version_tuple < (8, 1, 1) 22 | SUPPORTS_PERF_TRAMPOLINE = sysconfig.get_config_var("PY_HAVE_PERF_TRAMPOLINE") == 1 23 | 24 | 25 | def get_git_relative_path(abs_path: Path) -> Path: 26 | """Get the path relative to the git root directory. If the path is not 27 | inside a git repository, the original path itself is returned. 28 | """ 29 | git_path = Path(abs_path).resolve() 30 | while ( 31 | git_path != git_path.parent 32 | ): # stops at root since parent of root is root itself 33 | if (git_path / ".git").exists(): 34 | return abs_path.resolve().relative_to(git_path) 35 | git_path = git_path.parent 36 | return abs_path 37 | 38 | 39 | def get_git_relative_uri_and_name(nodeid: str, pytest_rootdir: Path) -> tuple[str, str]: 40 | """Get the benchmark uri relative to the git root dir and the benchmark name. 41 | 42 | Args: 43 | nodeid (str): the pytest nodeid, for example: 44 | testing/test_excinfo.py::TestFormattedExcinfo::test_repr_source 45 | pytest_rootdir (str): the pytest root dir, for example: 46 | /home/user/gitrepo/folder 47 | 48 | Returns: 49 | str: the benchmark uri relative to the git root dir, for example: 50 | folder/testing/test_excinfo.py::TestFormattedExcinfo::test_repr_source 51 | 52 | """ 53 | file_path, bench_name = nodeid.split("::", 1) 54 | absolute_file_path = pytest_rootdir / Path(file_path) 55 | relative_git_path = get_git_relative_path(absolute_file_path) 56 | return (f"{str(relative_git_path)}::{bench_name}", bench_name) 57 | 58 | 59 | def get_environment_metadata() -> dict[str, dict]: 60 | return { 61 | "creator": { 62 | "name": "pytest-codspeed", 63 | "version": __semver_version__, 64 | "pid": os.getpid(), 65 | }, 66 | "python": { 67 | "sysconfig": sysconfig.get_config_vars(), 68 | "dependencies": { 69 | d.name: d.version for d in importlib_metadata.distributions() 70 | }, 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /tests/benchmarks/TheAlgorithms_bench/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodSpeedHQ/pytest-codspeed/3e26619fa5d36309c4fa9a6cb9176cadf1381989/tests/benchmarks/TheAlgorithms_bench/__init__.py -------------------------------------------------------------------------------- /tests/benchmarks/TheAlgorithms_bench/bit_manipulation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from bit_manipulation.binary_and_operator import binary_and 3 | from bit_manipulation.binary_coded_decimal import binary_coded_decimal 4 | from bit_manipulation.binary_count_setbits import binary_count_setbits 5 | from bit_manipulation.binary_count_trailing_zeros import binary_count_trailing_zeros 6 | from bit_manipulation.binary_or_operator import binary_or 7 | from bit_manipulation.binary_shifts import ( 8 | arithmetic_right_shift, 9 | logical_left_shift, 10 | logical_right_shift, 11 | ) 12 | from bit_manipulation.binary_twos_complement import twos_complement 13 | from bit_manipulation.binary_xor_operator import binary_xor 14 | from bit_manipulation.count_1s_brian_kernighan_method import get_1s_count 15 | from bit_manipulation.excess_3_code import excess_3_code 16 | from bit_manipulation.find_previous_power_of_two import find_previous_power_of_two 17 | from bit_manipulation.gray_code_sequence import gray_code 18 | from bit_manipulation.highest_set_bit import get_highest_set_bit_position 19 | from bit_manipulation.is_even import is_even 20 | from bit_manipulation.largest_pow_of_two_le_num import largest_pow_of_two_le_num 21 | from bit_manipulation.missing_number import find_missing_number 22 | from bit_manipulation.numbers_different_signs import different_signs 23 | from bit_manipulation.power_of_4 import power_of_4 24 | from bit_manipulation.reverse_bits import reverse_bit 25 | from bit_manipulation.single_bit_manipulation_operations import ( 26 | clear_bit, 27 | flip_bit, 28 | get_bit, 29 | is_bit_set, 30 | set_bit, 31 | ) 32 | from bit_manipulation.swap_all_odd_and_even_bits import swap_odd_even_bits 33 | 34 | 35 | @pytest.mark.parametrize("a, b", [(25, 32), (37, 50), (21, 30), (58, 73)]) 36 | def test_binary_and(benchmark, a, b): 37 | benchmark(binary_and, a, b) 38 | 39 | 40 | @pytest.mark.parametrize("a, b", [(25, 32), (37, 50), (21, 30), (58, 73)]) 41 | def test_binary_or(benchmark, a, b): 42 | benchmark(binary_or, a, b) 43 | 44 | 45 | @pytest.mark.parametrize("a, b", [(25, 32), (37, 50), (21, 30), (58, 73)]) 46 | def test_binary_xor(benchmark, a, b): 47 | benchmark(binary_xor, a, b) 48 | 49 | 50 | @pytest.mark.parametrize("a", [25, 36, 16, 58, 4294967295, 0]) 51 | def test_binary_count_setbits(benchmark, a): 52 | benchmark(binary_count_setbits, a) 53 | 54 | 55 | @pytest.mark.parametrize("a", [25, 36, 16, 58, 4294967296, 0]) 56 | def test_binary_count_trailing_zeros(benchmark, a): 57 | benchmark(binary_count_trailing_zeros, a) 58 | 59 | 60 | @pytest.mark.parametrize("a", [-1, -5, -17, -207]) 61 | def test_twos_complement(benchmark, a): 62 | benchmark(twos_complement, a) 63 | 64 | 65 | @pytest.mark.parametrize("a", [25, 37, 21, 58, 0, 256]) 66 | def test_get_1s_count(benchmark, a): 67 | benchmark(get_1s_count, a) 68 | 69 | 70 | @pytest.mark.parametrize("a", [25, 37, 21, 58, 0, 256]) 71 | def test_reverse_bit(benchmark, a): 72 | benchmark(reverse_bit, a) 73 | 74 | 75 | @pytest.mark.parametrize("number, position", [(0b1101, 1), (0b0, 5), (0b1111, 1)]) 76 | def test_set_bit(benchmark, number, position): 77 | benchmark(set_bit, number, position) 78 | 79 | 80 | @pytest.mark.parametrize("number, position", [(0b10010, 1), (0b0, 5)]) 81 | def test_clear_bit(benchmark, number, position): 82 | benchmark(clear_bit, number, position) 83 | 84 | 85 | @pytest.mark.parametrize("number, position", [(0b101, 1), (0b101, 0)]) 86 | def test_flip_bit(benchmark, number, position): 87 | benchmark(flip_bit, number, position) 88 | 89 | 90 | @pytest.mark.parametrize( 91 | "number, position", [(0b1010, 0), (0b1010, 1), (0b1010, 2), (0b1010, 3), (0b0, 17)] 92 | ) 93 | def test_is_bit_set(benchmark, number, position): 94 | benchmark(is_bit_set, number, position) 95 | 96 | 97 | @pytest.mark.parametrize( 98 | "number, position", [(0b1010, 0), (0b1010, 1), (0b1010, 2), (0b1010, 3)] 99 | ) 100 | def test_get_bit(benchmark, number, position): 101 | benchmark(get_bit, number, position) 102 | 103 | 104 | @pytest.mark.parametrize( 105 | "number, shift_amount", [(0, 1), (1, 1), (1, 5), (17, 2), (1983, 4)] 106 | ) 107 | def test_logical_left_shift(benchmark, number, shift_amount): 108 | benchmark(logical_left_shift, number, shift_amount) 109 | 110 | 111 | @pytest.mark.parametrize( 112 | "number, shift_amount", [(0, 1), (1, 1), (1, 5), (17, 2), (1983, 4)] 113 | ) 114 | def test_logical_right_shift(benchmark, number, shift_amount): 115 | benchmark(logical_right_shift, number, shift_amount) 116 | 117 | 118 | @pytest.mark.parametrize( 119 | "number, shift_amount", [(0, 1), (1, 1), (-1, 1), (17, 2), (-17, 2), (-1983, 4)] 120 | ) 121 | def test_arithmetic_right_shift(benchmark, number, shift_amount): 122 | benchmark(arithmetic_right_shift, number, shift_amount) 123 | 124 | 125 | @pytest.mark.parametrize("number", [0, 3, 2, 12, 987]) 126 | def test_binary_coded_decimal(benchmark, number): 127 | benchmark(binary_coded_decimal, number) 128 | 129 | 130 | @pytest.mark.parametrize("number", [0, 3, 2, 20, 120]) 131 | def test_excess_3_code(benchmark, number): 132 | benchmark(excess_3_code, number) 133 | 134 | 135 | @pytest.mark.parametrize("number", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17]) 136 | def test_find_previous_power_of_two(benchmark, number): 137 | benchmark(find_previous_power_of_two, number) 138 | 139 | 140 | @pytest.mark.parametrize("bit_count", [1, 2, 3]) 141 | def test_gray_code(benchmark, bit_count): 142 | benchmark(gray_code, bit_count) 143 | 144 | 145 | @pytest.mark.parametrize("number", [25, 37, 1, 4, 0]) 146 | def test_get_highest_set_bit_position(benchmark, number): 147 | benchmark(get_highest_set_bit_position, number) 148 | 149 | 150 | @pytest.mark.parametrize("number", [1, 4, 9, 15, 40, 100, 101]) 151 | def test_is_even(benchmark, number): 152 | benchmark(is_even, number) 153 | 154 | 155 | @pytest.mark.parametrize("number", [0, 1, 3, 15, 99, 178, 999999]) 156 | def test_largest_pow_of_two_le_num(benchmark, number): 157 | benchmark(largest_pow_of_two_le_num, number) 158 | 159 | 160 | @pytest.mark.parametrize( 161 | "nums", 162 | [ 163 | [0, 1, 3, 4], 164 | [4, 3, 1, 0], 165 | [-4, -3, -1, 0], 166 | [-2, 2, 1, 3, 0], 167 | [1, 3, 4, 5, 6], 168 | [6, 5, 4, 2, 1], 169 | [6, 1, 5, 3, 4], 170 | ], 171 | ) 172 | def test_find_missing_number(benchmark, nums): 173 | benchmark(find_missing_number, nums) 174 | 175 | 176 | @pytest.mark.parametrize( 177 | "num1, num2", 178 | [ 179 | (1, -1), 180 | (1, 1), 181 | (1000000000000000000000000000, -1000000000000000000000000000), 182 | (-1000000000000000000000000000, 1000000000000000000000000000), 183 | (50, 278), 184 | (0, 2), 185 | (2, 0), 186 | ], 187 | ) 188 | def test_different_signs(benchmark, num1, num2): 189 | benchmark(different_signs, num1, num2) 190 | 191 | 192 | @pytest.mark.parametrize("number", [1, 2, 4, 6, 8, 17, 64]) 193 | def test_power_of_4(benchmark, number): 194 | benchmark(power_of_4, number) 195 | 196 | 197 | @pytest.mark.parametrize("number", [0, 1, 2, 3, 4, 5, 6, 23, 24]) 198 | def test_swap_odd_even_bits(benchmark, number): 199 | benchmark(swap_odd_even_bits, number) 200 | -------------------------------------------------------------------------------- /tests/benchmarks/TheAlgorithms_bench/test_bench_audio_filters.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from audio_filters.butterworth_filter import ( 3 | make_allpass, 4 | make_bandpass, 5 | make_highpass, 6 | make_highshelf, 7 | make_lowpass, 8 | make_lowshelf, 9 | make_peak, 10 | ) 11 | from audio_filters.iir_filter import IIRFilter 12 | 13 | 14 | def test_make_lowpass(benchmark): 15 | benchmark(make_lowpass, 1000, 48000) 16 | 17 | 18 | def test_make_highpass(benchmark): 19 | benchmark(make_highpass, 1000, 48000) 20 | 21 | 22 | def test_make_bandpass(benchmark): 23 | benchmark(make_bandpass, 1000, 48000) 24 | 25 | 26 | def test_make_allpass(benchmark): 27 | benchmark(make_allpass, 1000, 48000) 28 | 29 | 30 | def test_make_peak(benchmark): 31 | benchmark(make_peak, 1000, 48000, 6) 32 | 33 | 34 | def test_make_lowshelf(benchmark): 35 | benchmark(make_lowshelf, 1000, 48000, 6) 36 | 37 | 38 | def test_make_highshelf(benchmark): 39 | benchmark(make_highshelf, 1000, 48000, 6) 40 | 41 | 42 | @pytest.mark.parametrize("a_coeffs, b_coeffs", [([1.0, -1.8, 0.81], [0.9, -1.8, 0.81])]) 43 | def test_iir_filter_set_coefficients(benchmark, a_coeffs, b_coeffs): 44 | filt = IIRFilter(2) 45 | benchmark(filt.set_coefficients, a_coeffs, b_coeffs) 46 | 47 | 48 | def test_iir_filter_process(benchmark): 49 | filt = IIRFilter(2) 50 | benchmark(filt.process, 0) 51 | -------------------------------------------------------------------------------- /tests/benchmarks/TheAlgorithms_bench/test_bench_backtracking.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import pytest 4 | from backtracking.all_combinations import combination_lists, generate_all_combinations 5 | from backtracking.all_permutations import generate_all_permutations 6 | from backtracking.all_subsequences import generate_all_subsequences 7 | from backtracking.coloring import color 8 | from backtracking.combination_sum import combination_sum 9 | from backtracking.crossword_puzzle_solver import solve_crossword 10 | from backtracking.generate_parentheses import generate_parenthesis 11 | from backtracking.hamiltonian_cycle import hamilton_cycle 12 | from backtracking.knight_tour import get_valid_pos, is_complete, open_knight_tour 13 | from backtracking.match_word_pattern import match_word_pattern 14 | from backtracking.minimax import minimax 15 | from backtracking.n_queens import is_safe 16 | from backtracking.n_queens import solve as n_queens_solve 17 | from backtracking.n_queens_math import depth_first_search 18 | from backtracking.power_sum import solve 19 | from backtracking.rat_in_maze import solve_maze 20 | from backtracking.sudoku import sudoku 21 | from backtracking.sum_of_subsets import generate_sum_of_subsets_soln 22 | from backtracking.word_search import word_exists 23 | 24 | 25 | @pytest.mark.parametrize("sequence", [[1, 2, 3], ["A", "B", "C"]]) 26 | def test_generate_all_permutations(benchmark, sequence): 27 | benchmark(generate_all_permutations, sequence) 28 | 29 | 30 | @pytest.mark.parametrize("n, k", [(4, 2), (0, 0), (5, 4)]) 31 | def test_combination_lists(benchmark, n, k): 32 | benchmark(combination_lists, n, k) 33 | 34 | 35 | @pytest.mark.parametrize("n, k", [(4, 2), (0, 0), (5, 4)]) 36 | def test_generate_all_combinations(benchmark, n, k): 37 | benchmark(generate_all_combinations, n, k) 38 | 39 | 40 | @pytest.mark.parametrize("sequence", [[3, 2, 1], ["A", "B"]]) 41 | def test_generate_all_subsequences(benchmark, sequence): 42 | benchmark(generate_all_subsequences, sequence) 43 | 44 | 45 | @pytest.mark.parametrize("candidates, target", [([2, 3, 5], 8)]) 46 | def test_combination_sum(benchmark, candidates, target): 47 | benchmark(combination_sum, candidates, target) 48 | 49 | 50 | @pytest.mark.parametrize( 51 | "initial_grid", 52 | [ 53 | [ 54 | [3, 0, 6, 5, 0, 8, 4, 0, 0], 55 | [5, 2, 0, 0, 0, 0, 0, 0, 0], 56 | [0, 8, 7, 0, 0, 0, 0, 3, 1], 57 | [0, 0, 3, 0, 1, 0, 0, 8, 0], 58 | [9, 0, 0, 8, 6, 3, 0, 0, 5], 59 | [0, 5, 0, 0, 9, 0, 6, 0, 0], 60 | [1, 3, 0, 0, 0, 0, 2, 5, 0], 61 | [0, 0, 0, 0, 0, 0, 0, 7, 4], 62 | [0, 0, 5, 2, 0, 6, 3, 0, 0], 63 | ] 64 | ], 65 | ) 66 | def test_sudoku(benchmark, initial_grid): 67 | benchmark(sudoku, initial_grid) 68 | 69 | 70 | @pytest.mark.parametrize("nums, max_sum", [([3, 34, 4, 12, 5, 2], 9)]) 71 | def test_generate_sum_of_subsets_soln(benchmark, nums, max_sum): 72 | benchmark(generate_sum_of_subsets_soln, nums, max_sum) 73 | 74 | 75 | @pytest.mark.parametrize("scores", [[90, 23, 6, 33, 21, 65, 123, 34423]]) 76 | def test_minimax(benchmark, scores): 77 | height = math.log(len(scores), 2) 78 | benchmark(minimax, 0, 0, True, scores, height) 79 | 80 | 81 | @pytest.mark.parametrize( 82 | "graph, max_colors", 83 | [ 84 | ( 85 | [ 86 | [0, 1, 0, 0, 0], 87 | [1, 0, 1, 0, 1], 88 | [0, 1, 0, 1, 0], 89 | [0, 1, 1, 0, 0], 90 | [0, 1, 0, 0, 0], 91 | ], 92 | 3, 93 | ) 94 | ], 95 | ) 96 | def test_color(benchmark, graph, max_colors): 97 | benchmark(color, graph, max_colors) 98 | 99 | 100 | @pytest.mark.parametrize("n", [3]) 101 | def test_generate_parenthesis(benchmark, n): 102 | benchmark(generate_parenthesis, n) 103 | 104 | 105 | @pytest.mark.parametrize("x, n", [(13, 2)]) 106 | def test_solve_power_sum(benchmark, x, n): 107 | benchmark(solve, x, n) 108 | 109 | 110 | @pytest.mark.parametrize("board, row, col", [([[0, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1)]) 111 | def test_is_safe(benchmark, board, row, col): 112 | benchmark(is_safe, board, row, col) 113 | 114 | 115 | @pytest.mark.parametrize("board", [[[0 for i in range(4)] for j in range(4)]]) 116 | def test_n_queens_solve(benchmark, board): 117 | benchmark(n_queens_solve, board, 0) 118 | 119 | 120 | @pytest.mark.parametrize("pattern, string", [("aba", "GraphTreesGraph")]) 121 | def test_match_word_pattern(benchmark, pattern, string): 122 | benchmark(match_word_pattern, pattern, string) 123 | 124 | 125 | @pytest.mark.parametrize("pos, board_size", [((1, 3), 4)]) 126 | def test_get_valid_pos(benchmark, pos, board_size): 127 | benchmark(get_valid_pos, pos, board_size) 128 | 129 | 130 | @pytest.mark.parametrize("board", [[[1]]]) 131 | def test_is_complete(benchmark, board): 132 | benchmark(is_complete, board) 133 | 134 | 135 | @pytest.mark.parametrize("board_size", [1]) 136 | def test_open_knight_tour(benchmark, board_size): 137 | benchmark(open_knight_tour, board_size) 138 | 139 | 140 | @pytest.mark.parametrize( 141 | "graph", 142 | [ 143 | [ 144 | [0, 1, 0, 1, 0], 145 | [1, 0, 1, 1, 1], 146 | [0, 1, 0, 0, 1], 147 | [1, 1, 0, 0, 1], 148 | [0, 1, 1, 1, 0], 149 | ] 150 | ], 151 | ) 152 | def test_hamilton_cycle(benchmark, graph): 153 | benchmark(hamilton_cycle, graph) 154 | 155 | 156 | @pytest.mark.parametrize( 157 | "maze", 158 | [ 159 | [ 160 | [0, 1, 0, 1, 1], 161 | [0, 0, 0, 0, 0], 162 | [1, 0, 1, 0, 1], 163 | [0, 0, 1, 0, 0], 164 | [1, 0, 0, 1, 0], 165 | ] 166 | ], 167 | ) 168 | def test_solve_maze(benchmark, maze): 169 | benchmark(solve_maze, maze, 0, 0, len(maze) - 1, len(maze) - 1) 170 | 171 | 172 | @pytest.mark.parametrize( 173 | "board, word", 174 | [([["A", "B", "C", "E"], ["S", "F", "C", "S"], ["A", "D", "E", "E"]], "ABCCED")], 175 | ) 176 | def test_word_exists(benchmark, board, word): 177 | benchmark(word_exists, board, word) 178 | 179 | 180 | @pytest.mark.parametrize( 181 | "puzzle, words", [([[""] * 3 for _ in range(3)], ["cat", "dog", "car"])] 182 | ) 183 | def test_solve_crossword(benchmark, puzzle, words): 184 | benchmark(solve_crossword, puzzle, words) 185 | 186 | 187 | @pytest.mark.parametrize("n", [4]) 188 | def test_depth_first_search(benchmark, n): 189 | boards = [] 190 | benchmark(depth_first_search, [], [], [], boards, n) 191 | -------------------------------------------------------------------------------- /tests/benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodSpeedHQ/pytest-codspeed/3e26619fa5d36309c4fa9a6cb9176cadf1381989/tests/benchmarks/__init__.py -------------------------------------------------------------------------------- /tests/benchmarks/test_bench_doc.py: -------------------------------------------------------------------------------- 1 | """Benches from the CodSpeed Getting Started Documentation.""" 2 | 3 | import pytest 4 | 5 | 6 | def sum_of_squares_fast(arr) -> int: 7 | total = 0 8 | for x in arr: 9 | total += x * x 10 | return total 11 | 12 | 13 | def sum_of_squares_slow(arr) -> int: 14 | return sum(map(lambda x: x**2, arr)) # noqa: C417 15 | 16 | 17 | @pytest.mark.benchmark 18 | def test_sum_squares_fast(): 19 | assert sum_of_squares_fast(range(1000)) == 332833500 20 | 21 | 22 | @pytest.mark.benchmark 23 | def test_sum_squares_slow(): 24 | assert sum_of_squares_slow(range(1000)) == 332833500 25 | -------------------------------------------------------------------------------- /tests/benchmarks/test_bench_fibo.py: -------------------------------------------------------------------------------- 1 | def recursive_fibonacci(n: int) -> int: 2 | if n in [0, 1]: 3 | return n 4 | return recursive_fibonacci(n - 1) + recursive_fibonacci(n - 2) 5 | 6 | 7 | def recursive_cached_fibonacci(n: int) -> int: 8 | cache = {0: 0, 1: 1} 9 | 10 | def fibo(n) -> int: 11 | if n in cache: 12 | return cache[n] 13 | cache[n] = fibo(n - 1) + fibo(n - 2) 14 | return cache[n] 15 | 16 | return fibo(n) 17 | 18 | 19 | def iterative_fibonacci(n: int) -> int: 20 | a, b = 0, 1 21 | for _ in range(n): 22 | a, b = b, a + b 23 | return a 24 | 25 | 26 | def test_iterative_fibo_10(benchmark): 27 | @benchmark 28 | def _(): 29 | iterative_fibonacci(10) 30 | 31 | 32 | def test_recursive_fibo_10(benchmark): 33 | @benchmark 34 | def _(): 35 | recursive_fibonacci(10) 36 | 37 | 38 | def test_recursive_fibo_20(benchmark): 39 | @benchmark 40 | def _(): 41 | recursive_fibonacci(20) 42 | 43 | 44 | def test_recursive_cached_fibo_10(benchmark): 45 | @benchmark 46 | def _(): 47 | recursive_cached_fibonacci(10) 48 | 49 | 50 | def test_recursive_cached_fibo_100(benchmark): 51 | @benchmark 52 | def _(): 53 | recursive_cached_fibonacci(100) 54 | -------------------------------------------------------------------------------- /tests/benchmarks/test_bench_misc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def count_even_fast(arr): 5 | """Count the number of even numbers in an array.""" 6 | even = 0 7 | for x in arr: 8 | if x % 2 == 0: 9 | even += 1 10 | return even 11 | 12 | 13 | def count_even_slow(arr): 14 | """Count the number of even numbers in an array.""" 15 | return sum(1 for x in arr if x % 2 == 0) 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "func", 20 | [ 21 | count_even_fast, 22 | count_even_slow, 23 | ], 24 | ) 25 | def test_count_even(func, benchmark): 26 | assert benchmark(func, range(10_000)) == 5000 27 | 28 | 29 | def sum_of_squares_for_loop_product(arr) -> int: 30 | total = 0 31 | for x in arr: 32 | total += x * x 33 | return total 34 | 35 | 36 | def sum_of_squares_for_loop_power(arr) -> int: 37 | total = 0 38 | for x in arr: 39 | total += x**2 40 | return total 41 | 42 | 43 | def sum_of_squares_sum_labmda_product(arr) -> int: 44 | return sum(map(lambda x: x * x, arr)) # noqa: C417 45 | 46 | 47 | def sum_of_squares_sum_labmda_power(arr) -> int: 48 | return sum(map(lambda x: x**2, arr)) # noqa: C417 49 | 50 | 51 | def sum_of_squares_sum_comprehension_product(arr) -> int: 52 | return sum(x * x for x in arr) 53 | 54 | 55 | def sum_of_squares_sum_comprehension_power(arr) -> int: 56 | return sum(x**2 for x in arr) 57 | 58 | 59 | @pytest.mark.parametrize( 60 | "func", 61 | [ 62 | sum_of_squares_for_loop_product, 63 | sum_of_squares_for_loop_power, 64 | sum_of_squares_sum_labmda_product, 65 | sum_of_squares_sum_labmda_power, 66 | sum_of_squares_sum_comprehension_product, 67 | sum_of_squares_sum_comprehension_power, 68 | ], 69 | ) 70 | @pytest.mark.benchmark 71 | def test_sum_of_squares(func): 72 | assert func(range(1000)) == 332833500 73 | -------------------------------------------------------------------------------- /tests/benchmarks/test_bench_syscalls.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import mmap 3 | import multiprocessing 4 | import os 5 | import socket 6 | from socket import gethostbyname 7 | from tempfile import NamedTemporaryFile 8 | from time import sleep 9 | 10 | import pytest 11 | 12 | 13 | @pytest.mark.parametrize("sleep_time", [0.001, 0.01, 0.05, 0.1]) 14 | def test_sleep(benchmark, sleep_time): 15 | benchmark(sleep, sleep_time) 16 | 17 | 18 | @pytest.mark.parametrize("array_size", [100, 1_000, 10_000, 100_000]) 19 | def test_array_alloc(benchmark, array_size): 20 | benchmark(lambda: [0] * array_size) 21 | 22 | 23 | @pytest.mark.parametrize("num_fds", [10, 100, 1000]) 24 | def test_open_close_fd(benchmark, num_fds): 25 | def open_close_fds(): 26 | fds = [os.open("/dev/null", os.O_RDONLY) for _ in range(num_fds)] 27 | for fd in fds: 28 | os.close(fd) 29 | 30 | benchmark(open_close_fds) 31 | 32 | 33 | def test_dup_fd(benchmark): 34 | def dup_fd(): 35 | fd = os.open("/dev/null", os.O_RDONLY) 36 | new_fd = os.dup(fd) 37 | os.close(new_fd) 38 | os.close(fd) 39 | 40 | benchmark(dup_fd) 41 | 42 | 43 | @pytest.mark.parametrize("content_length", [100, 1000, 10_000, 100_000, 1_000_000]) 44 | def test_fs_write(benchmark, content_length): 45 | content = "a" * content_length 46 | f = NamedTemporaryFile(mode="w") 47 | 48 | @benchmark 49 | def write_to_file(): 50 | f.write(content) 51 | f.flush() 52 | 53 | f.close() 54 | 55 | 56 | @pytest.mark.parametrize("content_length", [100, 1000, 10_000, 100_000, 1_000_000]) 57 | def test_fs_read(benchmark, content_length): 58 | with open("/dev/urandom", "rb") as f: 59 | benchmark(f.read, content_length) 60 | 61 | 62 | @pytest.mark.parametrize( 63 | "host", 64 | ["localhost", "127.0.0.1", "1.1.1.1", "8.8.8.8", "google.com", "amazon.com"], 65 | ) 66 | def test_hostname_resolution(benchmark, host): 67 | benchmark(gethostbyname, host) 68 | 69 | 70 | @pytest.mark.parametrize( 71 | "host, port", 72 | [("8.8.8.8", 53), ("1.1.1.1", 53), ("google.com", 443), ("wikipedia.org", 443)], 73 | ) 74 | def test_tcp_connection(benchmark, host, port): 75 | def connect(): 76 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 77 | try: 78 | sock.connect((host, port)) 79 | finally: 80 | sock.close() 81 | 82 | benchmark(connect) 83 | 84 | 85 | @pytest.mark.parametrize("command", ["echo hello", "ls -l", "cat /dev/null"]) 86 | def test_process_creation(benchmark, command): 87 | def create_process(): 88 | process = os.popen(command) 89 | process.read() 90 | process.close() 91 | 92 | benchmark(create_process) 93 | 94 | 95 | @pytest.mark.parametrize("message_size", [10, 100, 1000, 10000]) 96 | def test_pipe_communication(benchmark, message_size): 97 | def pipe_comm(): 98 | r, w = os.pipe() 99 | pid = os.fork() 100 | if pid == 0: # child process 101 | os.close(r) 102 | os.write(w, b"x" * message_size) 103 | os._exit(0) 104 | else: # parent process 105 | os.close(w) 106 | os.read(r, message_size) 107 | os.waitpid(pid, 0) 108 | os.close(r) 109 | 110 | benchmark(pipe_comm) 111 | 112 | 113 | @pytest.mark.parametrize("map_size", [4096, 40960, 409600]) 114 | def test_mmap_operation(benchmark, map_size): 115 | # Create a temporary file outside the benchmarked function 116 | temp_file = NamedTemporaryFile(mode="w+b", delete=False) 117 | temp_file.write(b"\0" * map_size) 118 | temp_file.flush() 119 | temp_file.close() 120 | 121 | mfd = os.open(temp_file.name, os.O_RDONLY) 122 | 123 | def mmap_op(): 124 | mm = mmap.mmap(mfd, map_size, access=mmap.ACCESS_READ) 125 | mm.read(map_size) 126 | 127 | benchmark(mmap_op) 128 | os.close(mfd) 129 | 130 | 131 | def multi_task(x): 132 | """Multiprocessing need this function to be defined at the top level.""" 133 | return x * x 134 | 135 | 136 | @pytest.mark.parametrize("num_tasks", [10, 100, 1000, 10000, 100000]) 137 | def test_threadpool_map(benchmark, num_tasks): 138 | def threadpool_map(): 139 | with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: 140 | list(executor.map(multi_task, range(num_tasks))) 141 | 142 | benchmark(threadpool_map) 143 | 144 | 145 | @pytest.mark.parametrize("num_tasks", [10, 100, 1000, 10000, 100000]) 146 | def test_multiprocessing_map(benchmark, num_tasks): 147 | def multiprocessing_map(): 148 | with multiprocessing.Pool(processes=8) as pool: 149 | list(pool.map(multi_task, range(num_tasks))) 150 | 151 | benchmark(multiprocessing_map) 152 | -------------------------------------------------------------------------------- /tests/benchmarks/test_bench_various_noop.py: -------------------------------------------------------------------------------- 1 | def noop_pass(): 2 | pass 3 | 4 | 5 | def noop_ellipsis(): ... 6 | 7 | 8 | def noop_lambda(): 9 | (lambda: None)() 10 | 11 | 12 | def test_noop_pass(benchmark): 13 | benchmark(noop_pass) 14 | 15 | 16 | def test_noop_ellipsis(benchmark): 17 | benchmark(noop_ellipsis) 18 | 19 | 20 | def test_noop_lambda(benchmark): 21 | benchmark(noop_lambda) 22 | 23 | 24 | def test_noop_pass_decorated(benchmark): 25 | @benchmark 26 | def _(): 27 | noop_pass() 28 | 29 | 30 | def test_noop_ellipsis_decorated(benchmark): 31 | @benchmark 32 | def _(): 33 | noop_ellipsis() 34 | 35 | 36 | def test_noop_lambda_decorated(benchmark): 37 | @benchmark 38 | def _(): 39 | noop_lambda() 40 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib.util 4 | import os 5 | import shutil 6 | import sys 7 | from contextlib import contextmanager 8 | from typing import TYPE_CHECKING 9 | 10 | import pytest 11 | 12 | from pytest_codspeed.instruments import MeasurementMode 13 | from pytest_codspeed.utils import IS_PYTEST_BENCHMARK_INSTALLED 14 | 15 | if TYPE_CHECKING: 16 | from _pytest.pytester import RunResult 17 | 18 | pytest_plugins = ["pytester"] 19 | 20 | skip_without_pytest_benchmark = pytest.mark.skipif( 21 | not IS_PYTEST_BENCHMARK_INSTALLED, reason="pytest_benchmark not installed" 22 | ) 23 | skip_with_pytest_benchmark = pytest.mark.skipif( 24 | IS_PYTEST_BENCHMARK_INSTALLED, reason="pytest_benchmark installed" 25 | ) 26 | if IS_PYTEST_BENCHMARK_INSTALLED: 27 | pytest_plugins.append("pytest_benchmark") 28 | print( 29 | "NOTICE: Testing with pytest-benchmark compatibility", 30 | file=sys.stderr, 31 | flush=True, 32 | ) 33 | 34 | IS_VALGRIND_INSTALLED = shutil.which("valgrind") is not None 35 | skip_without_valgrind = pytest.mark.skipif( 36 | "PYTEST_CODSPEED_FORCE_VALGRIND_TESTS" not in os.environ 37 | and not IS_VALGRIND_INSTALLED, 38 | reason="valgrind not installed", 39 | ) 40 | 41 | if IS_VALGRIND_INSTALLED: 42 | print("NOTICE: Testing with valgrind compatibility", file=sys.stderr, flush=True) 43 | 44 | IS_PERF_TRAMPOLINE_SUPPORTED = sys.version_info >= (3, 12) 45 | skip_without_perf_trampoline = pytest.mark.skipif( 46 | not IS_PERF_TRAMPOLINE_SUPPORTED, reason="perf trampoline is not supported" 47 | ) 48 | 49 | skip_with_perf_trampoline = pytest.mark.skipif( 50 | IS_PERF_TRAMPOLINE_SUPPORTED, reason="perf trampoline is supported" 51 | ) 52 | 53 | # The name for the pytest-xdist plugin is just "xdist" 54 | IS_PYTEST_XDIST_INSTALLED = importlib.util.find_spec("xdist") is not None 55 | skip_without_pytest_xdist = pytest.mark.skipif( 56 | not IS_PYTEST_XDIST_INSTALLED, 57 | reason="pytest_xdist not installed", 58 | ) 59 | 60 | 61 | @pytest.fixture(scope="function") 62 | def codspeed_env(monkeypatch): 63 | @contextmanager 64 | def ctx_manager(): 65 | monkeypatch.setenv("CODSPEED_ENV", "1") 66 | try: 67 | yield 68 | finally: 69 | monkeypatch.delenv("CODSPEED_ENV", raising=False) 70 | 71 | return ctx_manager 72 | 73 | 74 | def run_pytest_codspeed_with_mode( 75 | pytester: pytest.Pytester, mode: MeasurementMode, *args, **kwargs 76 | ) -> RunResult: 77 | csargs = [ 78 | "--codspeed", 79 | f"--codspeed-mode={mode.value}", 80 | ] 81 | if mode == MeasurementMode.WallTime: 82 | # Run only 1 round to speed up the test times 83 | csargs.extend(["--codspeed-warmup-time=0", "--codspeed-max-rounds=2"]) 84 | return pytester.runpytest( 85 | *csargs, 86 | *args, 87 | **kwargs, 88 | ) 89 | -------------------------------------------------------------------------------- /tests/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodSpeedHQ/pytest-codspeed/3e26619fa5d36309c4fa9a6cb9176cadf1381989/tests/examples/__init__.py -------------------------------------------------------------------------------- /tests/examples/test_addition_fixture.py: -------------------------------------------------------------------------------- 1 | def test_some_addition_performance(benchmark): 2 | @benchmark 3 | def _(): 4 | return 1 + 1 5 | -------------------------------------------------------------------------------- /tests/test_pytest_plugin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from conftest import ( 3 | IS_PERF_TRAMPOLINE_SUPPORTED, 4 | MeasurementMode, 5 | run_pytest_codspeed_with_mode, 6 | skip_with_perf_trampoline, 7 | skip_without_pytest_benchmark, 8 | skip_without_valgrind, 9 | ) 10 | 11 | 12 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 13 | def test_plugin_enabled_with_kwargs( 14 | pytester: pytest.Pytester, mode: MeasurementMode 15 | ) -> None: 16 | pytester.makepyfile( 17 | """ 18 | def test_arg_kwarg_addition(benchmark): 19 | def fn(arg, kwarg=None): 20 | assert arg + kwarg == 40 21 | benchmark(fn, 25, kwarg=15) 22 | """ 23 | ) 24 | result = run_pytest_codspeed_with_mode(pytester, mode) 25 | result.assert_outcomes(passed=1) 26 | 27 | 28 | @skip_without_valgrind 29 | @skip_with_perf_trampoline 30 | def test_bench_enabled_header_without_perf( 31 | pytester: pytest.Pytester, codspeed_env 32 | ) -> None: 33 | pytester.copy_example("tests/examples/test_addition_fixture.py") 34 | with codspeed_env(): 35 | result = pytester.runpytest() 36 | result.stdout.fnmatch_lines( 37 | ["codspeed: * (enabled, mode: instrumentation, callgraph: not supported)"] 38 | ) 39 | 40 | 41 | @skip_without_valgrind 42 | def test_plugin_enabled_by_env(pytester: pytest.Pytester, codspeed_env) -> None: 43 | pytester.copy_example("tests/examples/test_addition_fixture.py") 44 | with codspeed_env(): 45 | result = pytester.runpytest() 46 | result.stdout.fnmatch_lines(["*1 benchmarked*", "*1 passed*"]) 47 | 48 | 49 | @skip_without_valgrind 50 | def test_plugin_enabled_and_env(pytester: pytest.Pytester, codspeed_env) -> None: 51 | pytester.copy_example("tests/examples/test_addition_fixture.py") 52 | with codspeed_env(): 53 | result = pytester.runpytest("--codspeed") 54 | result.stdout.fnmatch_lines(["*1 benchmarked*", "*1 passed*"]) 55 | 56 | 57 | @skip_without_valgrind 58 | def test_plugin_enabled_and_env_bench_run_once( 59 | pytester: pytest.Pytester, codspeed_env 60 | ) -> None: 61 | pytester.makepyfile( 62 | """ 63 | import pytest 64 | 65 | @pytest.mark.benchmark 66 | def test_noisy_bench_marked(): 67 | print() # make sure noise is on its own line 68 | print("I'm noisy marked!!!") 69 | print() 70 | 71 | def test_noisy_bench_fxt(benchmark): 72 | @benchmark 73 | def _(): 74 | print() # make sure noise is on its own line 75 | print("I'm noisy fixtured!!!") 76 | print() 77 | """ 78 | ) 79 | EXPECTED_OUTPUT_COUNT = 2 if IS_PERF_TRAMPOLINE_SUPPORTED else 1 80 | with codspeed_env(): 81 | run_result = pytester.runpytest("--codspeed", "-s") 82 | print(run_result.stdout.str()) 83 | assert run_result.outlines.count("I'm noisy marked!!!") == EXPECTED_OUTPUT_COUNT 84 | assert ( 85 | run_result.outlines.count("I'm noisy fixtured!!!") == EXPECTED_OUTPUT_COUNT 86 | ) 87 | 88 | 89 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 90 | def test_plugin_enabled_and_env_bench_hierachy_called( 91 | pytester: pytest.Pytester, mode: MeasurementMode 92 | ) -> None: 93 | pytester.makepyfile( 94 | """ 95 | import pytest 96 | import time 97 | 98 | class TestGroup: 99 | def setup_method(self): 100 | print(); print("Setup called") 101 | 102 | def teardown_method(self): 103 | print(); print("Teardown called") 104 | 105 | @pytest.mark.benchmark 106 | def test_child(self): 107 | time.sleep(0.1) # Avoids the test being too fast 108 | print(); print("Test called") 109 | 110 | """ 111 | ) 112 | result = run_pytest_codspeed_with_mode(pytester, mode, "-s") 113 | result.stdout.fnmatch_lines( 114 | [ 115 | "Setup called", 116 | "Test called", 117 | "Teardown called", 118 | ] 119 | ) 120 | 121 | 122 | def test_plugin_disabled(pytester: pytest.Pytester) -> None: 123 | pytester.copy_example("tests/examples/test_addition_fixture.py") 124 | result = pytester.runpytest() 125 | result.stdout.fnmatch_lines(["*1 passed*"]) 126 | 127 | 128 | @skip_without_valgrind 129 | def test_plugin_enabled_nothing_to_benchmark( 130 | pytester: pytest.Pytester, codspeed_env 131 | ) -> None: 132 | pytester.makepyfile( 133 | """ 134 | def test_some_addition_performance(): 135 | return 1 + 1 136 | """ 137 | ) 138 | with codspeed_env(): 139 | result = pytester.runpytest("--codspeed") 140 | result.stdout.fnmatch_lines(["*0 benchmarked*", "*1 deselected*"]) 141 | 142 | 143 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 144 | def test_plugin_only_benchmark_collection( 145 | pytester: pytest.Pytester, mode: MeasurementMode 146 | ) -> None: 147 | pytester.makepyfile( 148 | """ 149 | import pytest 150 | 151 | @pytest.mark.codspeed_benchmark 152 | def test_some_addition_performance(): 153 | return 1 + 1 154 | 155 | @pytest.mark.benchmark 156 | def test_some_addition_performance_shorthand(): 157 | return 1 + 1 158 | 159 | def test_some_wrapped_benchmark(benchmark): 160 | @benchmark 161 | def _(): 162 | hello = "hello" 163 | 164 | def test_another_useless_thing(): 165 | assert True 166 | """ 167 | ) 168 | collection_result = run_pytest_codspeed_with_mode(pytester, mode, "--collect-only") 169 | 170 | collection_result.stdout.fnmatch_lines_random( 171 | [ 172 | "**", 173 | "**", 174 | "**", 175 | ], 176 | ) 177 | collection_result.assert_outcomes( 178 | deselected=1, 179 | ) 180 | 181 | collection_result = run_pytest_codspeed_with_mode( 182 | pytester, mode, "--collect-only", "-k", "test_some_wrapped_benchmark" 183 | ) 184 | collection_result.stdout.fnmatch_lines_random( 185 | [ 186 | "**", 187 | ], 188 | ) 189 | collection_result.assert_outcomes( 190 | deselected=3, 191 | ) 192 | 193 | 194 | @skip_without_pytest_benchmark 195 | def test_pytest_benchmark_compatibility(pytester: pytest.Pytester) -> None: 196 | pytester.makepyfile( 197 | """ 198 | def test_some_wrapped_benchmark(benchmark): 199 | @benchmark 200 | def _(): 201 | hello = "hello" 202 | """ 203 | ) 204 | result = pytester.runpytest( 205 | "--benchmark-only", 206 | "--benchmark-max-time=0", 207 | "--benchmark-warmup-iterations=1", 208 | ) 209 | result.stdout.fnmatch_lines_random( 210 | [ 211 | "*benchmark: 1 tests*", 212 | "*Name*", 213 | "*test_some_wrapped_benchmark*", 214 | "*Legend:*", 215 | "*Outliers:*", 216 | "*OPS: Operations Per Second*", 217 | "*Outliers:*", 218 | "*1 passed*", 219 | ] 220 | ) 221 | 222 | 223 | def test_codspeed_marker_unexpected_args(pytester: pytest.Pytester) -> None: 224 | pytester.makepyfile( 225 | """ 226 | import pytest 227 | 228 | @pytest.mark.codspeed_benchmark( 229 | "positional_arg" 230 | ) 231 | def test_bench(): 232 | pass 233 | """ 234 | ) 235 | result = pytester.runpytest("--codspeed") 236 | assert result.ret == 1 237 | result.stdout.fnmatch_lines_random( 238 | ["*ValueError: Positional arguments are not allowed in the benchmark marker*"], 239 | ) 240 | 241 | 242 | def test_codspeed_marker_unexpected_kwargs(pytester: pytest.Pytester) -> None: 243 | pytester.makepyfile( 244 | """ 245 | import pytest 246 | 247 | @pytest.mark.codspeed_benchmark( 248 | not_allowed=True 249 | ) 250 | def test_bench(): 251 | pass 252 | """ 253 | ) 254 | result = pytester.runpytest("--codspeed") 255 | assert result.ret == 1 256 | result.stdout.fnmatch_lines_random( 257 | [ 258 | "*ValueError: Unknown kwargs passed to benchmark marker: not_allowed*", 259 | ], 260 | ) 261 | 262 | 263 | def test_pytest_benchmark_extra_info(pytester: pytest.Pytester) -> None: 264 | """https://pytest-benchmark.readthedocs.io/en/latest/usage.html#extra-info""" 265 | pytester.makepyfile( 266 | """ 267 | import time 268 | 269 | def test_my_stuff(benchmark): 270 | benchmark.extra_info['foo'] = 'bar' 271 | benchmark(time.sleep, 0.02) 272 | """ 273 | ) 274 | result = pytester.runpytest("--codspeed") 275 | assert result.ret == 0, "the run should have succeeded" 276 | 277 | 278 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 279 | def test_pytest_benchmark_return_value( 280 | pytester: pytest.Pytester, mode: MeasurementMode 281 | ) -> None: 282 | pytester.makepyfile( 283 | """ 284 | def calculate_something(): 285 | return 1 + 1 286 | 287 | def test_my_stuff(benchmark): 288 | value = benchmark(calculate_something) 289 | assert value == 2 290 | """ 291 | ) 292 | result = run_pytest_codspeed_with_mode(pytester, mode) 293 | assert result.ret == 0, "the run should have succeeded" 294 | 295 | 296 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 297 | def test_print(pytester: pytest.Pytester, mode: MeasurementMode) -> None: 298 | """Test print statements are captured by pytest (i.e., not printed to terminal in 299 | the middle of the progress bar) and only displayed after test run (on failures).""" 300 | pytester.makepyfile( 301 | """ 302 | import pytest, sys 303 | 304 | @pytest.mark.benchmark 305 | def test_print(): 306 | print("print to stdout") 307 | print("print to stderr", file=sys.stderr) 308 | """ 309 | ) 310 | result = run_pytest_codspeed_with_mode(pytester, mode) 311 | assert result.ret == 0, "the run should have succeeded" 312 | result.assert_outcomes(passed=1) 313 | result.stdout.no_fnmatch_line("*print to stdout*") 314 | result.stderr.no_fnmatch_line("*print to stderr*") 315 | 316 | 317 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 318 | def test_capsys(pytester: pytest.Pytester, mode: MeasurementMode): 319 | """Test print statements are captured by capsys (i.e., not printed to terminal in 320 | the middle of the progress bar) and can be inspected within test.""" 321 | pytester.makepyfile( 322 | """ 323 | import pytest, sys 324 | 325 | @pytest.mark.benchmark 326 | def test_capsys(capsys): 327 | print("print to stdout") 328 | print("print to stderr", file=sys.stderr) 329 | 330 | stdout, stderr = capsys.readouterr() 331 | 332 | assert stdout == "print to stdout\\n" 333 | assert stderr == "print to stderr\\n" 334 | """ 335 | ) 336 | result = run_pytest_codspeed_with_mode(pytester, mode) 337 | assert result.ret == 0, "the run should have succeeded" 338 | result.assert_outcomes(passed=1) 339 | result.stdout.no_fnmatch_line("*print to stdout*") 340 | result.stderr.no_fnmatch_line("*print to stderr*") 341 | 342 | 343 | @pytest.mark.xfail(reason="not supported by pytest-benchmark, see #78") 344 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 345 | def test_stateful_warmup_fixture( 346 | pytester: pytest.Pytester, mode: MeasurementMode 347 | ) -> None: 348 | """Test that the stateful warmup works correctly.""" 349 | pytester.makepyfile( 350 | """ 351 | import pytest 352 | 353 | def test_stateful_warmup(benchmark): 354 | has_run = False 355 | 356 | def b(): 357 | nonlocal has_run 358 | assert not has_run, "Benchmark ran multiple times without setup" 359 | has_run = True 360 | 361 | benchmark(b) 362 | """ 363 | ) 364 | result = run_pytest_codspeed_with_mode(pytester, mode) 365 | assert result.ret == 0, "the run should have succeeded" 366 | result.assert_outcomes(passed=1) 367 | 368 | 369 | @pytest.mark.xfail(reason="not supported by pytest-benchmark, see #78") 370 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 371 | def test_stateful_warmup_marker( 372 | pytester: pytest.Pytester, mode: MeasurementMode 373 | ) -> None: 374 | """Test that the stateful warmup marker works correctly.""" 375 | pytester.makepyfile( 376 | """ 377 | import pytest 378 | 379 | has_run = False 380 | 381 | @pytest.fixture(autouse=True) 382 | def fixture(): 383 | global has_run 384 | has_run = False 385 | 386 | 387 | @pytest.mark.benchmark 388 | def test_stateful_warmup_marker(): 389 | global has_run 390 | assert not has_run, "Benchmark ran multiple times without setup" 391 | has_run = True 392 | """ 393 | ) 394 | result = run_pytest_codspeed_with_mode(pytester, mode) 395 | assert result.ret == 0, "the run should have succeeded" 396 | result.assert_outcomes(passed=1) 397 | 398 | 399 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 400 | def test_benchmark_fixture_used_twice( 401 | pytester: pytest.Pytester, mode: MeasurementMode 402 | ) -> None: 403 | """Test that using the benchmark fixture twice in a test raises an error.""" 404 | pytester.makepyfile( 405 | """ 406 | def test_benchmark_used_twice(benchmark): 407 | def foo(): 408 | pass 409 | 410 | benchmark(foo) 411 | benchmark(foo) 412 | """ 413 | ) 414 | result = run_pytest_codspeed_with_mode(pytester, mode) 415 | assert result.ret == 1, "the run should have failed" 416 | result.stdout.fnmatch_lines( 417 | ["*RuntimeError: The benchmark fixture can only be used once per test*"] 418 | ) 419 | 420 | 421 | @pytest.mark.parametrize("mode", [*MeasurementMode]) 422 | def test_benchmark_fixture_used_normal_pedantic( 423 | pytester: pytest.Pytester, mode: MeasurementMode 424 | ) -> None: 425 | """Test that using the benchmark fixture twice in a test raises an error.""" 426 | pytester.makepyfile( 427 | """ 428 | def test_benchmark_used_twice(benchmark): 429 | def foo(): 430 | pass 431 | 432 | benchmark(foo) 433 | benchmark.pedantic(foo) 434 | """ 435 | ) 436 | result = run_pytest_codspeed_with_mode(pytester, mode) 437 | assert result.ret == 1, "the run should have failed" 438 | result.stdout.fnmatch_lines( 439 | ["*RuntimeError: The benchmark fixture can only be used once per test*"] 440 | ) 441 | -------------------------------------------------------------------------------- /tests/test_pytest_plugin_cpu_instrumentation.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from conftest import ( 5 | run_pytest_codspeed_with_mode, 6 | skip_with_pytest_benchmark, 7 | skip_without_perf_trampoline, 8 | skip_without_pytest_xdist, 9 | skip_without_valgrind, 10 | ) 11 | 12 | from pytest_codspeed.instruments import MeasurementMode 13 | 14 | 15 | @skip_without_valgrind 16 | @skip_without_perf_trampoline 17 | def test_bench_enabled_header_with_perf( 18 | pytester: pytest.Pytester, codspeed_env 19 | ) -> None: 20 | pytester.copy_example("tests/examples/test_addition_fixture.py") 21 | with codspeed_env(): 22 | result = pytester.runpytest() 23 | result.stdout.fnmatch_lines( 24 | ["codspeed: * (enabled, mode: instrumentation, callgraph: enabled)"] 25 | ) 26 | 27 | 28 | def test_plugin_enabled_cpu_instrumentation_without_env( 29 | pytester: pytest.Pytester, 30 | ) -> None: 31 | pytester.makepyfile( 32 | """ 33 | def test_some_addition_performance(benchmark): 34 | @benchmark 35 | def _(): 36 | return 1 + 1 37 | """ 38 | ) 39 | result = run_pytest_codspeed_with_mode(pytester, MeasurementMode.Instrumentation) 40 | result.stdout.fnmatch_lines( 41 | [ 42 | ( 43 | "*NOTICE: codspeed is enabled, but no " 44 | "performance measurement will be made*" 45 | ), 46 | "*1 benchmark tested*", 47 | "*1 passed*", 48 | ] 49 | ) 50 | 51 | 52 | @skip_without_valgrind 53 | @skip_without_perf_trampoline 54 | def test_perf_maps_generation(pytester: pytest.Pytester, codspeed_env) -> None: 55 | pytester.makepyfile( 56 | """ 57 | import pytest 58 | 59 | @pytest.mark.benchmark 60 | def test_some_addition_marked(): 61 | assert 1 + 1 62 | 63 | def test_some_addition_fixtured(benchmark): 64 | @benchmark 65 | def fixtured_child(): 66 | assert 1 + 1 67 | """ 68 | ) 69 | with codspeed_env(): 70 | result = pytester.runpytest("--codspeed") 71 | result.stdout.fnmatch_lines(["*2 benchmarked*", "*2 passed*"]) 72 | current_pid = os.getpid() 73 | perf_filepath = f"/tmp/perf-{current_pid}.map" 74 | print(perf_filepath) 75 | 76 | with open(perf_filepath) as perf_file: 77 | lines = perf_file.readlines() 78 | assert any( 79 | "py::ValgrindInstrument.measure..__codspeed_root_frame__" in line 80 | for line in lines 81 | ), "No root frame found in perf map" 82 | assert any("py::test_some_addition_marked" in line for line in lines), ( 83 | "No marked test frame found in perf map" 84 | ) 85 | assert any("py::test_some_addition_fixtured" in line for line in lines), ( 86 | "No fixtured test frame found in perf map" 87 | ) 88 | assert any( 89 | "py::test_some_addition_fixtured..fixtured_child" in line 90 | for line in lines 91 | ), "No fixtured child test frame found in perf map" 92 | 93 | 94 | @skip_without_valgrind 95 | @skip_with_pytest_benchmark 96 | @skip_without_pytest_xdist 97 | def test_pytest_xdist_concurrency_compatibility( 98 | pytester: pytest.Pytester, codspeed_env 99 | ) -> None: 100 | pytester.makepyfile( 101 | """ 102 | import time, pytest 103 | 104 | def do_something(): 105 | time.sleep(1) 106 | 107 | @pytest.mark.parametrize("i", range(256)) 108 | def test_my_stuff(benchmark, i): 109 | benchmark(do_something) 110 | """ 111 | ) 112 | # Run the test multiple times to reduce the chance of a false positive 113 | ITERATIONS = 5 114 | for i in range(ITERATIONS): 115 | with codspeed_env(): 116 | result = pytester.runpytest("--codspeed", "-n", "128") 117 | assert result.ret == 0, "the run should have succeeded" 118 | result.stdout.fnmatch_lines(["*256 passed*"]) 119 | 120 | 121 | def test_valgrind_pedantic_warning(pytester: pytest.Pytester) -> None: 122 | """ 123 | Test that using pedantic mode with Valgrind instrumentation shows a warning about 124 | ignoring rounds and iterations. 125 | """ 126 | pytester.makepyfile( 127 | """ 128 | def test_benchmark_pedantic(benchmark): 129 | def foo(): 130 | return 1 + 1 131 | 132 | benchmark.pedantic(foo, rounds=10, iterations=100) 133 | """ 134 | ) 135 | result = run_pytest_codspeed_with_mode(pytester, MeasurementMode.Instrumentation) 136 | result.stdout.fnmatch_lines( 137 | [ 138 | "*UserWarning: Valgrind instrument ignores rounds and iterations settings " 139 | "in pedantic mode*" 140 | ] 141 | ) 142 | result.assert_outcomes(passed=1) 143 | 144 | 145 | @skip_without_valgrind 146 | @skip_without_perf_trampoline 147 | def test_benchmark_pedantic_instrumentation( 148 | pytester: pytest.Pytester, codspeed_env 149 | ) -> None: 150 | """Test that pedantic mode works with instrumentation mode.""" 151 | pytester.makepyfile( 152 | """ 153 | def test_pedantic_full_features(benchmark): 154 | setup_calls = 0 155 | teardown_calls = 0 156 | target_calls = 0 157 | 158 | def setup(): 159 | nonlocal setup_calls 160 | setup_calls += 1 161 | return (1, 2), {"c": 3} 162 | 163 | def teardown(a, b, c): 164 | nonlocal teardown_calls 165 | teardown_calls += 1 166 | assert a == 1 167 | assert b == 2 168 | assert c == 3 169 | 170 | def target(a, b, c): 171 | nonlocal target_calls 172 | target_calls += 1 173 | assert a == 1 174 | assert b == 2 175 | assert c == 3 176 | return a + b + c 177 | 178 | result = benchmark.pedantic( 179 | target, 180 | setup=setup, 181 | teardown=teardown, 182 | rounds=3, 183 | warmup_rounds=3 184 | ) 185 | 186 | # Verify the results 187 | # Instrumentation ignores rounds but is called during warmup 188 | assert result == 6 # 1 + 2 + 3 189 | assert setup_calls == 1 + 3 190 | assert teardown_calls == 1 + 3 191 | assert target_calls == 1 + 3 192 | """ 193 | ) 194 | with codspeed_env(): 195 | result = run_pytest_codspeed_with_mode( 196 | pytester, MeasurementMode.Instrumentation 197 | ) 198 | assert result.ret == 0, "the run should have succeeded" 199 | result.assert_outcomes(passed=1) 200 | -------------------------------------------------------------------------------- /tests/test_pytest_plugin_walltime.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from conftest import run_pytest_codspeed_with_mode 3 | 4 | from pytest_codspeed.instruments import MeasurementMode 5 | 6 | 7 | def test_bench_enabled_header_with_perf( 8 | pytester: pytest.Pytester, 9 | ) -> None: 10 | pytester.copy_example("tests/examples/test_addition_fixture.py") 11 | result = run_pytest_codspeed_with_mode(pytester, MeasurementMode.WallTime) 12 | result.stdout.fnmatch_lines(["*test_some_addition_performance*", "*1 benchmarked*"]) 13 | 14 | 15 | def test_parametrization_naming( 16 | pytester: pytest.Pytester, 17 | ) -> None: 18 | pytester.makepyfile( 19 | """ 20 | import time, pytest 21 | 22 | @pytest.mark.parametrize("inp", ["toto", 12, 58.3]) 23 | def test_my_stuff(benchmark, inp): 24 | benchmark(lambda: time.sleep(0.01)) 25 | """ 26 | ) 27 | result = run_pytest_codspeed_with_mode(pytester, MeasurementMode.WallTime) 28 | # Make sure the parametrization is not broken 29 | print(result.outlines) 30 | result.stdout.fnmatch_lines_random( 31 | [ 32 | "*test_my_stuff[[]toto[]]*", 33 | "*test_my_stuff[[]12[]]*", 34 | "*test_my_stuff[[]58.3[]]*", 35 | "*3 benchmarked*", 36 | ] 37 | ) 38 | 39 | 40 | def test_benchmark_pedantic_walltime( 41 | pytester: pytest.Pytester, 42 | ) -> None: 43 | """Test that pedantic mode works with walltime mode.""" 44 | pytester.makepyfile( 45 | """ 46 | def test_pedantic_full_features(benchmark): 47 | setup_calls = 0 48 | teardown_calls = 0 49 | target_calls = 0 50 | 51 | def setup(): 52 | nonlocal setup_calls 53 | setup_calls += 1 54 | return (1, 2), {"c": 3} 55 | 56 | def teardown(a, b, c): 57 | nonlocal teardown_calls 58 | teardown_calls += 1 59 | assert a == 1 60 | assert b == 2 61 | assert c == 3 62 | 63 | def target(a, b, c): 64 | nonlocal target_calls 65 | target_calls += 1 66 | assert a == 1 67 | assert b == 2 68 | assert c == 3 69 | return a + b + c 70 | 71 | result = benchmark.pedantic( 72 | target, 73 | setup=setup, 74 | teardown=teardown, 75 | rounds=3, 76 | warmup_rounds=1 77 | ) 78 | 79 | # Verify the results 80 | assert result == 6 # 1 + 2 + 3 81 | assert setup_calls == 5 # 3 rounds + 1 warmup + 1 calibration 82 | assert teardown_calls == 5 83 | assert target_calls == 5 84 | """ 85 | ) 86 | result = run_pytest_codspeed_with_mode(pytester, MeasurementMode.WallTime) 87 | assert result.ret == 0, "the run should have succeeded" 88 | result.assert_outcomes(passed=1) 89 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from contextlib import contextmanager 3 | from pathlib import Path 4 | 5 | from pytest_codspeed.utils import get_git_relative_path, get_git_relative_uri_and_name 6 | 7 | 8 | @contextmanager 9 | def TemporaryGitRepo(): 10 | with tempfile.TemporaryDirectory() as tmpdirname: 11 | (Path(tmpdirname) / ".git").mkdir(parents=True) 12 | yield tmpdirname 13 | 14 | 15 | def test_get_git_relative_path_found(): 16 | with TemporaryGitRepo() as tmp_repo: 17 | path = Path(tmp_repo) / "folder/nested_folder" 18 | assert get_git_relative_path(path) == Path("folder/nested_folder") 19 | 20 | 21 | def test_get_git_relative_path_not_found(): 22 | with tempfile.TemporaryDirectory() as tmp_dir: 23 | path = Path(tmp_dir) / "folder" 24 | assert get_git_relative_path(path) == path 25 | 26 | 27 | def test_get_git_relative_uri(): 28 | with TemporaryGitRepo() as tmp_repo: 29 | pytest_rootdir = Path(tmp_repo) / "pytest_root" 30 | uri = "testing/test_excinfo.py::TestFormattedExcinfo::test_fn" 31 | assert get_git_relative_uri_and_name(uri, pytest_rootdir) == ( 32 | "pytest_root/testing/test_excinfo.py::TestFormattedExcinfo::test_fn", 33 | "TestFormattedExcinfo::test_fn", 34 | ) 35 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.9" 3 | 4 | [[package]] 5 | name = "cffi" 6 | version = "1.17.1" 7 | source = { registry = "https://pypi.org/simple" } 8 | dependencies = [ 9 | { name = "pycparser" }, 10 | ] 11 | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } 12 | wheels = [ 13 | { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, 14 | { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, 15 | { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, 16 | { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, 17 | { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, 18 | { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, 19 | { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, 20 | { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, 21 | { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, 22 | { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, 23 | { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, 24 | { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, 25 | { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, 26 | { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, 27 | { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, 28 | { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, 29 | { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, 30 | { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, 31 | { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, 32 | { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, 33 | { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, 34 | { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, 35 | { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, 36 | { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, 37 | { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, 38 | { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, 39 | { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, 40 | { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, 41 | { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, 42 | { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, 43 | { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, 44 | { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, 45 | { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, 46 | { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, 47 | { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, 48 | { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, 49 | { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, 50 | { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, 51 | { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, 52 | { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, 53 | { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, 54 | { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, 55 | { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, 56 | { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, 57 | { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, 58 | { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, 59 | { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, 60 | { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, 61 | { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, 62 | { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, 63 | { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, 64 | { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, 65 | { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, 66 | { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, 67 | { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, 68 | { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, 69 | { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, 70 | { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, 71 | ] 72 | 73 | [[package]] 74 | name = "colorama" 75 | version = "0.4.6" 76 | source = { registry = "https://pypi.org/simple" } 77 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 78 | wheels = [ 79 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 80 | ] 81 | 82 | [[package]] 83 | name = "coverage" 84 | version = "7.6.8" 85 | source = { registry = "https://pypi.org/simple" } 86 | sdist = { url = "https://files.pythonhosted.org/packages/ab/75/aecfd0a3adbec6e45753976bc2a9fed62b42cea9a206d10fd29244a77953/coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", size = 801425 } 87 | wheels = [ 88 | { url = "https://files.pythonhosted.org/packages/31/86/6ed22e101badc8eedf181f0c2f65500df5929c44c79991cf45b9bf741424/coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50", size = 206988 }, 89 | { url = "https://files.pythonhosted.org/packages/3b/04/16853c58bacc02b3ff5405193dfc6c66632442d931b23dd7b9452dc55cf3/coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf", size = 207418 }, 90 | { url = "https://files.pythonhosted.org/packages/f8/eb/8a91520d04215eb549d6a7d7d3a79cbb1d78b5dd0814f4b23bf97521d580/coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee", size = 235860 }, 91 | { url = "https://files.pythonhosted.org/packages/00/10/bf1ede5b54ae1bbf39921a5dd4cc84aee79041ed301ec8955064785ddb90/coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6", size = 233766 }, 92 | { url = "https://files.pythonhosted.org/packages/5c/ea/741d9233eb502906e0d18ccf4c15c4fb74ff0e85fd8ee967590194b889a1/coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d", size = 234924 }, 93 | { url = "https://files.pythonhosted.org/packages/18/43/b2cfd4413a5b64ab27c289228b0c45b4527d1b99381cc9d6a00bfd515da4/coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331", size = 234019 }, 94 | { url = "https://files.pythonhosted.org/packages/8e/95/8b2fbb9d1a79277963b6095cd51a90fb7088cd3618faf75550038331f78b/coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638", size = 232481 }, 95 | { url = "https://files.pythonhosted.org/packages/4d/d7/9e939508a39ef67605b715ca89c6522214aceb27c2db9152ae3ae1cf8626/coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed", size = 233609 }, 96 | { url = "https://files.pythonhosted.org/packages/ba/e2/1c5fb52eafcffeebaa9db084bff47e7c3cf4f97db752226c232cee4d530b/coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e", size = 209669 }, 97 | { url = "https://files.pythonhosted.org/packages/31/31/6a56469609a252549dd4b090815428d5521edd4642440d987573a450c069/coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a", size = 210509 }, 98 | { url = "https://files.pythonhosted.org/packages/ab/9f/e98211980f6e2f439e251737482aa77906c9b9c507824c71a2ce7eea0402/coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", size = 207093 }, 99 | { url = "https://files.pythonhosted.org/packages/fd/c7/8bab83fb9c20f7f8163c5a20dcb62d591b906a214a6dc6b07413074afc80/coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", size = 207536 }, 100 | { url = "https://files.pythonhosted.org/packages/1e/d6/00243df625f1b282bb25c83ce153ae2c06f8e7a796a8d833e7235337b4d9/coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", size = 239482 }, 101 | { url = "https://files.pythonhosted.org/packages/1e/07/faf04b3eeb55ffc2a6f24b65dffe6e0359ec3b283e6efb5050ea0707446f/coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", size = 236886 }, 102 | { url = "https://files.pythonhosted.org/packages/43/23/c79e497bf4d8fcacd316bebe1d559c765485b8ec23ac4e23025be6bfce09/coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", size = 238749 }, 103 | { url = "https://files.pythonhosted.org/packages/b5/e5/791bae13be3c6451e32ef7af1192e711c6a319f3c597e9b218d148fd0633/coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", size = 237679 }, 104 | { url = "https://files.pythonhosted.org/packages/05/c6/bbfdfb03aada601fb8993ced17468c8c8e0b4aafb3097026e680fabb7ce1/coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", size = 236317 }, 105 | { url = "https://files.pythonhosted.org/packages/67/f9/f8e5a4b2ce96d1b0e83ae6246369eb8437001dc80ec03bb51c87ff557cd8/coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", size = 237084 }, 106 | { url = "https://files.pythonhosted.org/packages/f0/70/b05328901e4debe76e033717e1452d00246c458c44e9dbd893e7619c2967/coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", size = 209638 }, 107 | { url = "https://files.pythonhosted.org/packages/70/55/1efa24f960a2fa9fbc44a9523d3f3c50ceb94dd1e8cd732168ab2dc41b07/coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9", size = 210506 }, 108 | { url = "https://files.pythonhosted.org/packages/76/ce/3edf581c8fe429ed8ced6e6d9ac693c25975ef9093413276dab6ed68a80a/coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", size = 207285 }, 109 | { url = "https://files.pythonhosted.org/packages/09/9c/cf102ab046c9cf8895c3f7aadcde6f489a4b2ec326757e8c6e6581829b5e/coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", size = 207522 }, 110 | { url = "https://files.pythonhosted.org/packages/39/06/42aa6dd13dbfca72e1fd8ffccadbc921b6e75db34545ebab4d955d1e7ad3/coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", size = 240543 }, 111 | { url = "https://files.pythonhosted.org/packages/a0/20/2932971dc215adeca8eeff446266a7fef17a0c238e881ffedebe7bfa0669/coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", size = 237577 }, 112 | { url = "https://files.pythonhosted.org/packages/ac/85/4323ece0cd5452c9522f4b6e5cc461e6c7149a4b1887c9e7a8b1f4e51146/coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", size = 239646 }, 113 | { url = "https://files.pythonhosted.org/packages/77/52/b2537487d8f36241e518e84db6f79e26bc3343b14844366e35b090fae0d4/coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", size = 239128 }, 114 | { url = "https://files.pythonhosted.org/packages/7c/99/7f007762012186547d0ecc3d328da6b6f31a8c99f05dc1e13dcd929918cd/coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", size = 237434 }, 115 | { url = "https://files.pythonhosted.org/packages/97/53/e9b5cf0682a1cab9352adfac73caae0d77ae1d65abc88975d510f7816389/coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", size = 239095 }, 116 | { url = "https://files.pythonhosted.org/packages/0c/50/054f0b464fbae0483217186478eefa2e7df3a79917ed7f1d430b6da2cf0d/coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", size = 209895 }, 117 | { url = "https://files.pythonhosted.org/packages/df/d0/09ba870360a27ecf09e177ca2ff59d4337fc7197b456f22ceff85cffcfa5/coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", size = 210684 }, 118 | { url = "https://files.pythonhosted.org/packages/9a/84/6f0ccf94a098ac3d6d6f236bd3905eeac049a9e0efcd9a63d4feca37ac4b/coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", size = 207313 }, 119 | { url = "https://files.pythonhosted.org/packages/db/2b/e3b3a3a12ebec738c545897ac9f314620470fcbc368cdac88cf14974ba20/coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", size = 207574 }, 120 | { url = "https://files.pythonhosted.org/packages/db/c0/5bf95d42b6a8d21dfce5025ce187f15db57d6460a59b67a95fe8728162f1/coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", size = 240090 }, 121 | { url = "https://files.pythonhosted.org/packages/57/b8/d6fd17d1a8e2b0e1a4e8b9cb1f0f261afd422570735899759c0584236916/coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", size = 237237 }, 122 | { url = "https://files.pythonhosted.org/packages/d4/e4/a91e9bb46809c8b63e68fc5db5c4d567d3423b6691d049a4f950e38fbe9d/coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", size = 239225 }, 123 | { url = "https://files.pythonhosted.org/packages/31/9c/9b99b0591ec4555b7292d271e005f27b465388ce166056c435b288db6a69/coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", size = 238888 }, 124 | { url = "https://files.pythonhosted.org/packages/a6/85/285c2df9a04bc7c31f21fd9d4a24d19e040ec5e2ff06e572af1f6514c9e7/coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", size = 236974 }, 125 | { url = "https://files.pythonhosted.org/packages/cb/a1/95ec8522206f76cdca033bf8bb61fff56429fb414835fc4d34651dfd29fc/coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", size = 238815 }, 126 | { url = "https://files.pythonhosted.org/packages/8d/ac/687e9ba5e6d0979e9dab5c02e01c4f24ac58260ef82d88d3b433b3f84f1e/coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", size = 209957 }, 127 | { url = "https://files.pythonhosted.org/packages/2f/a3/b61cc8e3fcf075293fb0f3dee405748453c5ba28ac02ceb4a87f52bdb105/coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", size = 210711 }, 128 | { url = "https://files.pythonhosted.org/packages/ee/4b/891c8b9acf1b62c85e4a71dac142ab9284e8347409b7355de02e3f38306f/coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", size = 208053 }, 129 | { url = "https://files.pythonhosted.org/packages/18/a9/9e330409b291cc002723d339346452800e78df1ce50774ca439ade1d374f/coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", size = 208329 }, 130 | { url = "https://files.pythonhosted.org/packages/9c/0d/33635fd429f6589c6e1cdfc7bf581aefe4c1792fbff06383f9d37f59db60/coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", size = 251052 }, 131 | { url = "https://files.pythonhosted.org/packages/23/32/8a08da0e46f3830bbb9a5b40614241b2e700f27a9c2889f53122486443ed/coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", size = 246765 }, 132 | { url = "https://files.pythonhosted.org/packages/56/3f/3b86303d2c14350fdb1c6c4dbf9bc76000af2382f42ca1d4d99c6317666e/coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", size = 249125 }, 133 | { url = "https://files.pythonhosted.org/packages/36/cb/c4f081b9023f9fd8646dbc4ef77be0df090263e8f66f4ea47681e0dc2cff/coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", size = 248615 }, 134 | { url = "https://files.pythonhosted.org/packages/32/ee/53bdbf67760928c44b57b2c28a8c0a4bf544f85a9ee129a63ba5c78fdee4/coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", size = 246507 }, 135 | { url = "https://files.pythonhosted.org/packages/57/49/5a57910bd0af6d8e802b4ca65292576d19b54b49f81577fd898505dee075/coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", size = 247785 }, 136 | { url = "https://files.pythonhosted.org/packages/bd/37/e450c9f6b297c79bb9858407396ed3e084dcc22990dd110ab01d5ceb9770/coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", size = 210605 }, 137 | { url = "https://files.pythonhosted.org/packages/44/79/7d0c7dd237c6905018e2936cd1055fe1d42e7eba2ebab3c00f4aad2a27d7/coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", size = 211777 }, 138 | { url = "https://files.pythonhosted.org/packages/2e/db/5c7008bcd8858c2dea02702ef0fee761f23780a6be7cd1292840f3e165b1/coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e", size = 206983 }, 139 | { url = "https://files.pythonhosted.org/packages/1c/30/e1be5b6802baa55967e83bdf57bd51cd2763b72cdc591a90aa0b465fffee/coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c", size = 207422 }, 140 | { url = "https://files.pythonhosted.org/packages/f6/df/19c0e12f9f7b976cd7b92ae8200d26f5b6cd3f322d17ac7b08d48fbf5bc5/coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0", size = 235455 }, 141 | { url = "https://files.pythonhosted.org/packages/e8/7a/a80b0c4fb48e8bce92bcfe3908e47e6c7607fb8f618a4e0de78218e42d9b/coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779", size = 233376 }, 142 | { url = "https://files.pythonhosted.org/packages/8c/0e/1a4ecee734d70b78fc458ff611707f804605721467ef45fc1f1a684772ad/coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92", size = 234509 }, 143 | { url = "https://files.pythonhosted.org/packages/24/42/6eadd73adc0163cb18dee4fef80baefeb3faa11a1e217a2db80e274e784d/coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4", size = 233659 }, 144 | { url = "https://files.pythonhosted.org/packages/68/5f/10b825f39ecfe6fc5ee3120205daaa0950443948f0d0a538430f386fdf58/coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc", size = 232138 }, 145 | { url = "https://files.pythonhosted.org/packages/56/72/ad92bdad934de103e19a128a349ef4a0560892fd33d62becb1140885e44c/coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea", size = 233131 }, 146 | { url = "https://files.pythonhosted.org/packages/f4/1d/d61d9b2d17628c4db834e9650b776663535b4258d0dc204ec475188b5b2a/coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e", size = 209695 }, 147 | { url = "https://files.pythonhosted.org/packages/0f/d1/ef43852a998c41183dbffed4ab0dd658f9975d570c6106ea43fdcb5dcbf4/coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076", size = 210475 }, 148 | { url = "https://files.pythonhosted.org/packages/32/df/0d2476121cd0bfb9ca2413efe02289c474b82c4b134863bef4b89ec7bcfa/coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce", size = 199230 }, 149 | ] 150 | 151 | [package.optional-dependencies] 152 | toml = [ 153 | { name = "tomli", marker = "python_full_version <= '3.11'" }, 154 | ] 155 | 156 | [[package]] 157 | name = "exceptiongroup" 158 | version = "1.2.2" 159 | source = { registry = "https://pypi.org/simple" } 160 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 161 | wheels = [ 162 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 163 | ] 164 | 165 | [[package]] 166 | name = "execnet" 167 | version = "2.1.1" 168 | source = { registry = "https://pypi.org/simple" } 169 | sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524 } 170 | wheels = [ 171 | { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, 172 | ] 173 | 174 | [[package]] 175 | name = "importlib-metadata" 176 | version = "8.5.0" 177 | source = { registry = "https://pypi.org/simple" } 178 | dependencies = [ 179 | { name = "zipp" }, 180 | ] 181 | sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } 182 | wheels = [ 183 | { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, 184 | ] 185 | 186 | [[package]] 187 | name = "iniconfig" 188 | version = "2.0.0" 189 | source = { registry = "https://pypi.org/simple" } 190 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 191 | wheels = [ 192 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 193 | ] 194 | 195 | [[package]] 196 | name = "markdown-it-py" 197 | version = "3.0.0" 198 | source = { registry = "https://pypi.org/simple" } 199 | dependencies = [ 200 | { name = "mdurl" }, 201 | ] 202 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 203 | wheels = [ 204 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 205 | ] 206 | 207 | [[package]] 208 | name = "mdurl" 209 | version = "0.1.2" 210 | source = { registry = "https://pypi.org/simple" } 211 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 212 | wheels = [ 213 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 214 | ] 215 | 216 | [[package]] 217 | name = "mypy" 218 | version = "1.11.2" 219 | source = { registry = "https://pypi.org/simple" } 220 | dependencies = [ 221 | { name = "mypy-extensions" }, 222 | { name = "tomli", marker = "python_full_version < '3.11'" }, 223 | { name = "typing-extensions" }, 224 | ] 225 | sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806 } 226 | wheels = [ 227 | { url = "https://files.pythonhosted.org/packages/78/cd/815368cd83c3a31873e5e55b317551500b12f2d1d7549720632f32630333/mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", size = 10939401 }, 228 | { url = "https://files.pythonhosted.org/packages/f1/27/e18c93a195d2fad75eb96e1f1cbc431842c332e8eba2e2b77eaf7313c6b7/mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", size = 10111697 }, 229 | { url = "https://files.pythonhosted.org/packages/dc/08/cdc1fc6d0d5a67d354741344cc4aa7d53f7128902ebcbe699ddd4f15a61c/mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", size = 12500508 }, 230 | { url = "https://files.pythonhosted.org/packages/64/12/aad3af008c92c2d5d0720ea3b6674ba94a98cdb86888d389acdb5f218c30/mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", size = 13020712 }, 231 | { url = "https://files.pythonhosted.org/packages/03/e6/a7d97cc124a565be5e9b7d5c2a6ebf082379ffba99646e4863ed5bbcb3c3/mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", size = 9567319 }, 232 | { url = "https://files.pythonhosted.org/packages/e2/aa/cc56fb53ebe14c64f1fe91d32d838d6f4db948b9494e200d2f61b820b85d/mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", size = 10859630 }, 233 | { url = "https://files.pythonhosted.org/packages/04/c8/b19a760fab491c22c51975cf74e3d253b8c8ce2be7afaa2490fbf95a8c59/mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", size = 10037973 }, 234 | { url = "https://files.pythonhosted.org/packages/88/57/7e7e39f2619c8f74a22efb9a4c4eff32b09d3798335625a124436d121d89/mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", size = 12416659 }, 235 | { url = "https://files.pythonhosted.org/packages/fc/a6/37f7544666b63a27e46c48f49caeee388bf3ce95f9c570eb5cfba5234405/mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", size = 12897010 }, 236 | { url = "https://files.pythonhosted.org/packages/84/8b/459a513badc4d34acb31c736a0101c22d2bd0697b969796ad93294165cfb/mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", size = 9562873 }, 237 | { url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335 }, 238 | { url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119 }, 239 | { url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856 }, 240 | { url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066 }, 241 | { url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000 }, 242 | { url = "https://files.pythonhosted.org/packages/16/64/bb5ed751487e2bea0dfaa6f640a7e3bb88083648f522e766d5ef4a76f578/mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", size = 10937294 }, 243 | { url = "https://files.pythonhosted.org/packages/a9/a3/67a0069abed93c3bf3b0bebb8857e2979a02828a4a3fd82f107f8f1143e8/mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", size = 10107707 }, 244 | { url = "https://files.pythonhosted.org/packages/2f/4d/0379daf4258b454b1f9ed589a9dabd072c17f97496daea7b72fdacf7c248/mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d", size = 12498367 }, 245 | { url = "https://files.pythonhosted.org/packages/3b/dc/3976a988c280b3571b8eb6928882dc4b723a403b21735a6d8ae6ed20e82b/mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", size = 13018014 }, 246 | { url = "https://files.pythonhosted.org/packages/83/84/adffc7138fb970e7e2a167bd20b33bb78958370179853a4ebe9008139342/mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", size = 9568056 }, 247 | { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625 }, 248 | ] 249 | 250 | [[package]] 251 | name = "mypy-extensions" 252 | version = "1.0.0" 253 | source = { registry = "https://pypi.org/simple" } 254 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } 255 | wheels = [ 256 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, 257 | ] 258 | 259 | [[package]] 260 | name = "packaging" 261 | version = "24.2" 262 | source = { registry = "https://pypi.org/simple" } 263 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 264 | wheels = [ 265 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 266 | ] 267 | 268 | [[package]] 269 | name = "pluggy" 270 | version = "1.5.0" 271 | source = { registry = "https://pypi.org/simple" } 272 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 273 | wheels = [ 274 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 275 | ] 276 | 277 | [[package]] 278 | name = "py-cpuinfo" 279 | version = "9.0.0" 280 | source = { registry = "https://pypi.org/simple" } 281 | sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716 } 282 | wheels = [ 283 | { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335 }, 284 | ] 285 | 286 | [[package]] 287 | name = "pycparser" 288 | version = "2.22" 289 | source = { registry = "https://pypi.org/simple" } 290 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } 291 | wheels = [ 292 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, 293 | ] 294 | 295 | [[package]] 296 | name = "pygments" 297 | version = "2.18.0" 298 | source = { registry = "https://pypi.org/simple" } 299 | sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } 300 | wheels = [ 301 | { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, 302 | ] 303 | 304 | [[package]] 305 | name = "pytest" 306 | version = "7.4.4" 307 | source = { registry = "https://pypi.org/simple" } 308 | dependencies = [ 309 | { name = "colorama", marker = "sys_platform == 'win32'" }, 310 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 311 | { name = "iniconfig" }, 312 | { name = "packaging" }, 313 | { name = "pluggy" }, 314 | { name = "tomli", marker = "python_full_version < '3.11'" }, 315 | ] 316 | sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116 } 317 | wheels = [ 318 | { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287 }, 319 | ] 320 | 321 | [[package]] 322 | name = "pytest-benchmark" 323 | version = "5.0.1" 324 | source = { registry = "https://pypi.org/simple" } 325 | dependencies = [ 326 | { name = "py-cpuinfo" }, 327 | { name = "pytest" }, 328 | ] 329 | sdist = { url = "https://files.pythonhosted.org/packages/a3/48/b79272b2b8938513a66a62204a0649ef730dcf6cb52c812f4dc4daa62cd5/pytest-benchmark-5.0.1.tar.gz", hash = "sha256:8138178618c85586ce056c70cc5e92f4283c2e6198e8422c2c825aeb3ace6afd", size = 337310 } 330 | wheels = [ 331 | { url = "https://files.pythonhosted.org/packages/f7/e2/c0da4989a933d6bac364f215217c47de37d2f641953aa69a37b66efd6d1b/pytest_benchmark-5.0.1-py3-none-any.whl", hash = "sha256:d75fec4cbf0d4fd91e020f425ce2d845e9c127c21bae35e77c84db8ed84bfaa6", size = 44062 }, 332 | ] 333 | 334 | [[package]] 335 | name = "pytest-codspeed" 336 | source = { editable = "." } 337 | dependencies = [ 338 | { name = "cffi" }, 339 | { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, 340 | { name = "pytest" }, 341 | { name = "rich" }, 342 | ] 343 | 344 | [package.optional-dependencies] 345 | compat = [ 346 | { name = "pytest-benchmark" }, 347 | { name = "pytest-xdist" }, 348 | ] 349 | lint = [ 350 | { name = "mypy" }, 351 | { name = "ruff" }, 352 | ] 353 | test = [ 354 | { name = "pytest" }, 355 | { name = "pytest-cov" }, 356 | ] 357 | 358 | [package.dev-dependencies] 359 | dev = [ 360 | { name = "pytest-codspeed" }, 361 | ] 362 | 363 | [package.metadata] 364 | requires-dist = [ 365 | { name = "cffi", specifier = ">=1.17.1" }, 366 | { name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=8.5.0" }, 367 | { name = "mypy", marker = "extra == 'lint'", specifier = "~=1.11.2" }, 368 | { name = "pytest", specifier = ">=3.8" }, 369 | { name = "pytest", marker = "extra == 'test'", specifier = "~=7.0" }, 370 | { name = "pytest-benchmark", marker = "extra == 'compat'", specifier = "~=5.0.0" }, 371 | { name = "pytest-cov", marker = "extra == 'test'", specifier = "~=4.0.0" }, 372 | { name = "pytest-xdist", marker = "extra == 'compat'", specifier = "~=3.6.1" }, 373 | { name = "rich", specifier = ">=13.8.1" }, 374 | { name = "ruff", marker = "extra == 'lint'", specifier = "~=0.11.12" }, 375 | ] 376 | 377 | [package.metadata.requires-dev] 378 | dev = [{ name = "pytest-codspeed", editable = "." }] 379 | 380 | [[package]] 381 | name = "pytest-cov" 382 | version = "4.0.0" 383 | source = { registry = "https://pypi.org/simple" } 384 | dependencies = [ 385 | { name = "coverage", extra = ["toml"] }, 386 | { name = "pytest" }, 387 | ] 388 | sdist = { url = "https://files.pythonhosted.org/packages/ea/70/da97fd5f6270c7d2ce07559a19e5bf36a76f0af21500256f005a69d9beba/pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470", size = 62013 } 389 | wheels = [ 390 | { url = "https://files.pythonhosted.org/packages/fe/1f/9ec0ddd33bd2b37d6ec50bb39155bca4fe7085fa78b3b434c05459a860e3/pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b", size = 21554 }, 391 | ] 392 | 393 | [[package]] 394 | name = "pytest-xdist" 395 | version = "3.6.1" 396 | source = { registry = "https://pypi.org/simple" } 397 | dependencies = [ 398 | { name = "execnet" }, 399 | { name = "pytest" }, 400 | ] 401 | sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060 } 402 | wheels = [ 403 | { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108 }, 404 | ] 405 | 406 | [[package]] 407 | name = "rich" 408 | version = "13.9.4" 409 | source = { registry = "https://pypi.org/simple" } 410 | dependencies = [ 411 | { name = "markdown-it-py" }, 412 | { name = "pygments" }, 413 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 414 | ] 415 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } 416 | wheels = [ 417 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, 418 | ] 419 | 420 | [[package]] 421 | name = "ruff" 422 | version = "0.11.12" 423 | source = { registry = "https://pypi.org/simple" } 424 | sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289 } 425 | wheels = [ 426 | { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597 }, 427 | { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154 }, 428 | { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048 }, 429 | { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062 }, 430 | { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152 }, 431 | { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067 }, 432 | { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807 }, 433 | { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261 }, 434 | { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601 }, 435 | { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186 }, 436 | { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032 }, 437 | { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370 }, 438 | { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529 }, 439 | { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642 }, 440 | { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511 }, 441 | { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573 }, 442 | { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770 }, 443 | ] 444 | 445 | [[package]] 446 | name = "tomli" 447 | version = "2.2.1" 448 | source = { registry = "https://pypi.org/simple" } 449 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 450 | wheels = [ 451 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 452 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 453 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 454 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 455 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 456 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 457 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 458 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 459 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 460 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 461 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 462 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 463 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 464 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 465 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 466 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 467 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 468 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 469 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 470 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 471 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 472 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 473 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 474 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 475 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 476 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 477 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 478 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 479 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 480 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 481 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 482 | ] 483 | 484 | [[package]] 485 | name = "typing-extensions" 486 | version = "4.12.2" 487 | source = { registry = "https://pypi.org/simple" } 488 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 489 | wheels = [ 490 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 491 | ] 492 | 493 | [[package]] 494 | name = "zipp" 495 | version = "3.21.0" 496 | source = { registry = "https://pypi.org/simple" } 497 | sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } 498 | wheels = [ 499 | { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, 500 | ] 501 | --------------------------------------------------------------------------------