├── .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 | [](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml)
5 | [](https://pypi.org/project/pytest-codspeed)
6 | 
7 | [](https://discord.com/invite/MxpaCfKSqF)
8 | [](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 |
--------------------------------------------------------------------------------