From fbae262c19df2f7ebe7d8d5ee15d7f957b97ada7 Mon Sep 17 00:00:00 2001 From: Adrien Cacciaguerra Date: Thu, 9 Jan 2025 13:07:00 +0000 Subject: [PATCH] feat: store more benchmark metadata in results Use json reprensation as benchmark identifier in valgrind instrumentation --- pyproject.toml | 2 +- src/pytest_codspeed/benchmark.py | 55 +++++++++ src/pytest_codspeed/instruments/__init__.py | 4 +- .../instruments/valgrind/__init__.py | 11 +- src/pytest_codspeed/instruments/walltime.py | 46 ++++---- src/pytest_codspeed/plugin.py | 26 ++--- src/pytest_codspeed/utils.py | 20 ---- tests/conftest.py | 5 +- tests/test_benchmark.py | 22 ++++ tests/test_pytest_plugin_results_json.py | 82 ++++++++++++++ tests/test_utils.py | 12 +- uv.lock | 106 ++++++++++++++++++ 12 files changed, 317 insertions(+), 74 deletions(-) create mode 100644 src/pytest_codspeed/benchmark.py create mode 100644 tests/test_benchmark.py create mode 100644 tests/test_pytest_plugin_results_json.py diff --git a/pyproject.toml b/pyproject.toml index 40e652e..c12d436 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ compat = [ "pytest-xdist ~= 3.6.1", # "pytest-speed>=0.3.5", ] -test = ["pytest ~= 7.0", "pytest-cov ~= 4.0.0"] +test = ["inline-snapshot>=0.18.2", "pytest ~= 7.0", "pytest-cov ~= 4.0.0"] [project.entry-points] pytest11 = { codspeed = "pytest_codspeed.plugin" } diff --git a/src/pytest_codspeed/benchmark.py b/src/pytest_codspeed/benchmark.py new file mode 100644 index 0000000..309b03b --- /dev/null +++ b/src/pytest_codspeed/benchmark.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass + +import pytest + +from pytest_codspeed.utils import get_git_relative_path + + +def has_args(item: pytest.Item) -> bool: + return isinstance(item, pytest.Function) and "callspec" in item.__dict__ + + +@dataclass +class Benchmark: + file: str + module: str + groups: list[str] + name: str + args: list + args_names: list[str] + + @classmethod + def from_item(cls, item: pytest.Item) -> Benchmark: + file = str(get_git_relative_path(item.path)) + module = "::".join( + [node.name for node in item.listchain() if isinstance(node, pytest.Class)] + ) + name = item.originalname if isinstance(item, pytest.Function) else item.name + args = list(item.callspec.params.values()) if has_args(item) else [] + args_names = list(item.callspec.params.keys()) if has_args(item) else [] + groups = [] + benchmark_marker = item.get_closest_marker("benchmark") + if benchmark_marker is not None: + benchmark_marker_kwargs = benchmark_marker.kwargs.get("group") + if benchmark_marker_kwargs is not None: + groups.append(benchmark_marker_kwargs) + + return cls( + file=file, + module=module, + groups=groups, + name=name, + args=args, + args_names=args_names, + ) + + @property + def display_name(self) -> str: + args_str = f"[{'-'.join(map(str, self.args))}]" if len(self.args) > 0 else "" + return f"{self.name}{args_str}" + + def to_json_string(self) -> str: + return json.dumps(self.__dict__, separators=(",", ":"), sort_keys=True) diff --git a/src/pytest_codspeed/instruments/__init__.py b/src/pytest_codspeed/instruments/__init__.py index edd2849..962a6cd 100644 --- a/src/pytest_codspeed/instruments/__init__.py +++ b/src/pytest_codspeed/instruments/__init__.py @@ -9,6 +9,7 @@ import pytest + from pytest_codspeed.benchmark import Benchmark from pytest_codspeed.plugin import CodSpeedConfig T = TypeVar("T") @@ -27,8 +28,7 @@ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: ... @abstractmethod def measure( self, - name: str, - uri: str, + benchmark: Benchmark, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs, diff --git a/src/pytest_codspeed/instruments/valgrind/__init__.py b/src/pytest_codspeed/instruments/valgrind/__init__.py index 9b80092..46e33b0 100644 --- a/src/pytest_codspeed/instruments/valgrind/__init__.py +++ b/src/pytest_codspeed/instruments/valgrind/__init__.py @@ -2,6 +2,7 @@ import os import sys +from dataclasses import asdict from typing import TYPE_CHECKING from pytest_codspeed import __semver_version__ @@ -13,6 +14,7 @@ from pytest import Session + from pytest_codspeed.benchmark import Benchmark from pytest_codspeed.instruments import P, T from pytest_codspeed.instruments.valgrind._wrapper import LibType from pytest_codspeed.plugin import CodSpeedConfig @@ -26,6 +28,7 @@ class ValgrindInstrument(Instrument): def __init__(self, config: CodSpeedConfig) -> None: self.benchmark_count = 0 + self.benchmarks: list[Benchmark] = [] self.should_measure = os.environ.get("CODSPEED_ENV") is not None if self.should_measure: self.lib = get_lib() @@ -54,13 +57,13 @@ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: def measure( self, - name: str, - uri: str, + benchmark: Benchmark, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs, ) -> T: self.benchmark_count += 1 + self.benchmarks.append(benchmark) if self.lib is None: # Thus should_measure is False return fn(*args, **kwargs) @@ -78,7 +81,7 @@ def __codspeed_root_frame__() -> T: finally: # Ensure instrumentation is stopped even if the test failed self.lib.stop_instrumentation() - self.lib.dump_stats_at(uri.encode("ascii")) + self.lib.dump_stats_at(benchmark.to_json_string().encode("ascii")) def report(self, session: Session) -> None: reporter = session.config.pluginmanager.get_plugin("terminalreporter") @@ -91,5 +94,5 @@ def report(self, session: Session) -> None: def get_result_dict(self) -> dict[str, Any]: return { "instrument": {"type": self.instrument}, - # bench results will be dumped by valgrind + "benchmarks": [asdict(bench) for bench in self.benchmarks], } diff --git a/src/pytest_codspeed/instruments/walltime.py b/src/pytest_codspeed/instruments/walltime.py index e107ca3..e44abac 100644 --- a/src/pytest_codspeed/instruments/walltime.py +++ b/src/pytest_codspeed/instruments/walltime.py @@ -11,6 +11,7 @@ from rich.table import Table from rich.text import Text +from pytest_codspeed.benchmark import Benchmark from pytest_codspeed.instruments import Instrument if TYPE_CHECKING: @@ -28,14 +29,14 @@ @dataclass -class BenchmarkConfig: +class WalltimeBenchmarkConfig: warmup_time_ns: int min_round_time_ns: float max_time_ns: int max_rounds: int | None @classmethod - def from_codspeed_config(cls, config: CodSpeedConfig) -> BenchmarkConfig: + def from_codspeed_config(cls, config: CodSpeedConfig) -> WalltimeBenchmarkConfig: return cls( warmup_time_ns=config.warmup_time_ns if config.warmup_time_ns is not None @@ -49,7 +50,7 @@ def from_codspeed_config(cls, config: CodSpeedConfig) -> BenchmarkConfig: @dataclass -class BenchmarkStats: +class WalltimeBenchmarkStats: min_ns: float max_ns: float mean_ns: float @@ -75,7 +76,7 @@ def from_list( iter_per_round: int, warmup_iters: int, total_time: float, - ) -> BenchmarkStats: + ) -> WalltimeBenchmarkStats: stdev_ns = stdev(times_ns) if len(times_ns) > 1 else 0 mean_ns = mean(times_ns) if len(times_ns) > 1: @@ -114,17 +115,18 @@ def from_list( @dataclass -class Benchmark: - name: str - uri: str - - config: BenchmarkConfig - stats: BenchmarkStats +class WalltimeBenchmark(Benchmark): + config: WalltimeBenchmarkConfig + stats: WalltimeBenchmarkStats def run_benchmark( - name: str, uri: str, fn: Callable[P, T], args, kwargs, config: BenchmarkConfig -) -> tuple[Benchmark, T]: + benchmark: Benchmark, + fn: Callable[P, T], + args, + kwargs, + config: WalltimeBenchmarkConfig, +) -> tuple[WalltimeBenchmark, T]: # Compute the actual result of the function out = fn(*args, **kwargs) @@ -171,7 +173,7 @@ def run_benchmark( benchmark_end = perf_counter_ns() total_time = (benchmark_end - run_start) / 1e9 - stats = BenchmarkStats.from_list( + stats = WalltimeBenchmarkStats.from_list( times_ns, rounds=rounds, total_time=total_time, @@ -179,7 +181,11 @@ def run_benchmark( warmup_iters=warmup_iters, ) - return Benchmark(name=name, uri=uri, config=config, stats=stats), out + return WalltimeBenchmark( + **asdict(benchmark), + config=config, + stats=stats, + ), out class WallTimeInstrument(Instrument): @@ -187,26 +193,24 @@ class WallTimeInstrument(Instrument): def __init__(self, config: CodSpeedConfig) -> None: self.config = config - self.benchmarks: list[Benchmark] = [] + self.benchmarks: list[WalltimeBenchmark] = [] def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: return f"mode: walltime, timer_resolution: {TIMER_RESOLUTION_NS:.1f}ns", [] def measure( self, - name: str, - uri: str, + benchmark: Benchmark, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs, ) -> T: bench, out = run_benchmark( - name=name, - uri=uri, + benchmark=benchmark, fn=fn, args=args, kwargs=kwargs, - config=BenchmarkConfig.from_codspeed_config(self.config), + config=WalltimeBenchmarkConfig.from_codspeed_config(self.config), ) self.benchmarks.append(bench) return out @@ -244,7 +248,7 @@ def _print_benchmark_table(self) -> None: if rsd > 0.1: rsd_text.stylize("red bold") table.add_row( - escape(bench.name), + escape(bench.display_name), f"{bench.stats.min_ns/bench.stats.iter_per_round:,.0f}ns", rsd_text, f"{bench.stats.total_time:,.2f}s", diff --git a/src/pytest_codspeed/plugin.py b/src/pytest_codspeed/plugin.py index 881b27d..a9532d1 100644 --- a/src/pytest_codspeed/plugin.py +++ b/src/pytest_codspeed/plugin.py @@ -13,13 +13,13 @@ import pytest from _pytest.fixtures import FixtureManager +from pytest_codspeed.benchmark import Benchmark from pytest_codspeed.instruments import ( MeasurementMode, get_instrument_from_mode, ) from pytest_codspeed.utils import ( get_environment_metadata, - get_git_relative_uri_and_name, ) from . import __version__ @@ -253,8 +253,7 @@ def pytest_collection_modifyitems( def _measure( plugin: CodSpeedPlugin, - nodeid: str, - config: pytest.Config, + item: pytest.Item, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs, @@ -264,8 +263,8 @@ def _measure( gc.collect() gc.disable() try: - uri, name = get_git_relative_uri_and_name(nodeid, config.rootpath) - return plugin.instrument.measure(name, uri, fn, *args, **kwargs) + benchmark = Benchmark.from_item(item) + return plugin.instrument.measure(benchmark, fn, *args, **kwargs) finally: # Ensure GC is re-enabled even if the test failed if is_gc_enabled: @@ -274,13 +273,13 @@ def _measure( def wrap_runtest( plugin: CodSpeedPlugin, - nodeid: str, - config: pytest.Config, - fn: Callable[P, T], + item: pytest.Item, ) -> Callable[P, T]: + fn = item.runtest + @functools.wraps(fn) def wrapped(*args: P.args, **kwargs: P.kwargs) -> T: - return _measure(plugin, nodeid, config, fn, *args, **kwargs) + return _measure(plugin, item, fn, *args, **kwargs) return wrapped @@ -297,7 +296,7 @@ def pytest_runtest_protocol(item: pytest.Item, nextitem: pytest.Item | None): return None # Wrap runtest and defer to default protocol - item.runtest = wrap_runtest(plugin, item.nodeid, item.config, item.runtest) + item.runtest = wrap_runtest(plugin, item) return None @@ -340,10 +339,9 @@ def __init__(self, request: pytest.FixtureRequest): def __call__(self, func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: config = self._request.config plugin = get_plugin(config) - if plugin.is_codspeed_enabled: - return _measure( - plugin, self._request.node.nodeid, config, func, *args, **kwargs - ) + item = self._request.node + if plugin.is_codspeed_enabled and isinstance(item, pytest.Item): + return _measure(plugin, item, func, *args, **kwargs) else: return func(*args, **kwargs) diff --git a/src/pytest_codspeed/utils.py b/src/pytest_codspeed/utils.py index 221505b..ded388d 100644 --- a/src/pytest_codspeed/utils.py +++ b/src/pytest_codspeed/utils.py @@ -27,26 +27,6 @@ def get_git_relative_path(abs_path: Path) -> Path: return abs_path -def get_git_relative_uri_and_name(nodeid: str, pytest_rootdir: Path) -> tuple[str, str]: - """Get the benchmark uri relative to the git root dir and the benchmark name. - - Args: - nodeid (str): the pytest nodeid, for example: - testing/test_excinfo.py::TestFormattedExcinfo::test_repr_source - pytest_rootdir (str): the pytest root dir, for example: - /home/user/gitrepo/folder - - Returns: - str: the benchmark uri relative to the git root dir, for example: - folder/testing/test_excinfo.py::TestFormattedExcinfo::test_repr_source - - """ - file_path, bench_name = nodeid.split("::", 1) - absolute_file_path = pytest_rootdir / Path(file_path) - relative_git_path = get_git_relative_path(absolute_file_path) - return (f"{str(relative_git_path)}::{bench_name}", bench_name) - - def get_environment_metadata() -> dict[str, dict]: return { "creator": { diff --git a/tests/conftest.py b/tests/conftest.py index aa2d077..782056f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -81,7 +81,10 @@ def run_pytest_codspeed_with_mode( if mode == MeasurementMode.WallTime: # Run only 1 round to speed up the test times csargs.extend(["--codspeed-warmup-time=0", "--codspeed-max-rounds=2"]) - return pytester.runpytest( + # create empty `.git` folder in the rootdir to simulate a git repository + if not pytester.path.joinpath(".git").exists(): + pytester.mkdir(".git") + return pytester.runpytest_subprocess( *csargs, *args, **kwargs, diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py new file mode 100644 index 0000000..7967d2c --- /dev/null +++ b/tests/test_benchmark.py @@ -0,0 +1,22 @@ +from inline_snapshot._inline_snapshot import snapshot + +from pytest_codspeed.plugin import Benchmark + +benchmark = Benchmark( + file="test_benchmark_results.py", + module="TestClass::TestNested", + groups=["bench-group"], + name="test_fully_parametrized", + args=[58.3, "baz"], + args_names=["a", "b"], +) + + +def test_benchmark_to_json_string(): + assert benchmark.to_json_string() == snapshot( + '{"args":[58.3,"baz"],"args_names":["a","b"],"file":"test_benchmark_results.py","groups":["bench-group"],"module":"TestClass::TestNested","name":"test_fully_parametrized"}' + ) + + +def test_benchmark_display_name(): + assert benchmark.display_name == "test_fully_parametrized[58.3-baz]" diff --git a/tests/test_pytest_plugin_results_json.py b/tests/test_pytest_plugin_results_json.py new file mode 100644 index 0000000..9ee713d --- /dev/null +++ b/tests/test_pytest_plugin_results_json.py @@ -0,0 +1,82 @@ +import json + +import pytest +from conftest import run_pytest_codspeed_with_mode +from inline_snapshot import snapshot + +from pytest_codspeed.instruments import MeasurementMode +from pytest_codspeed.instruments.valgrind import ValgrindInstrument +from pytest_codspeed.instruments.walltime import WallTimeInstrument + + +@pytest.mark.parametrize( + "mode", [MeasurementMode.WallTime, MeasurementMode.Instrumentation] +) +def test_benchmark_results(pytester: pytest.Pytester, mode: MeasurementMode) -> None: + pytester.makepyfile( + """ + import time, pytest + + def test_no_parametrization(benchmark): + benchmark(lambda: 1 + 1) + + class TestClass: + class TestNested: + @pytest.mark.benchmark(group="bench-group") + @pytest.mark.parametrize("a,b", [("foo","bar"), (12,13), (58.3,"baz")]) + def test_fully_parametrized(self, benchmark, a, b): + benchmark(lambda: 1 + 1) + """ + ) + result = run_pytest_codspeed_with_mode(pytester, mode) + result.stdout.fnmatch_lines_random(["*4 benchmark*"]) + + with open(next(pytester.path.joinpath(".codspeed").glob("*.json"))) as file: + results = json.loads(file.read()) + assert ( + results.get("instrument").get("type") == ValgrindInstrument.instrument + if mode == MeasurementMode.Instrumentation + else WallTimeInstrument.instrument + ) + + benchmarks = results.get("benchmarks") + if mode == MeasurementMode.WallTime: + for b in benchmarks: + del b["stats"] + del b["config"] + assert benchmarks == snapshot( + [ + { + "file": "test_benchmark_results.py", + "module": "", + "groups": [], + "name": "test_no_parametrization", + "args": [], + "args_names": [], + }, + { + "file": "test_benchmark_results.py", + "module": "TestClass::TestNested", + "groups": ["bench-group"], + "name": "test_fully_parametrized", + "args": ["foo", "bar"], + "args_names": ["a", "b"], + }, + { + "file": "test_benchmark_results.py", + "module": "TestClass::TestNested", + "groups": ["bench-group"], + "name": "test_fully_parametrized", + "args": [12, 13], + "args_names": ["a", "b"], + }, + { + "file": "test_benchmark_results.py", + "module": "TestClass::TestNested", + "groups": ["bench-group"], + "name": "test_fully_parametrized", + "args": [58.3, "baz"], + "args_names": ["a", "b"], + }, + ] + ) diff --git a/tests/test_utils.py b/tests/test_utils.py index c9d2e01..893cad8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from pathlib import Path -from pytest_codspeed.utils import get_git_relative_path, get_git_relative_uri_and_name +from pytest_codspeed.utils import get_git_relative_path @contextmanager @@ -22,13 +22,3 @@ def test_get_git_relative_path_not_found(): with tempfile.TemporaryDirectory() as tmp_dir: path = Path(tmp_dir) / "folder" assert get_git_relative_path(path) == path - - -def test_get_git_relative_uri(): - with TemporaryGitRepo() as tmp_repo: - pytest_rootdir = Path(tmp_repo) / "pytest_root" - uri = "testing/test_excinfo.py::TestFormattedExcinfo::test_fn" - assert get_git_relative_uri_and_name(uri, pytest_rootdir) == ( - "pytest_root/testing/test_excinfo.py::TestFormattedExcinfo::test_fn", - "TestFormattedExcinfo::test_fn", - ) diff --git a/uv.lock b/uv.lock index be41074..a395493 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,53 @@ version = 1 requires-python = ">=3.9" +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, +] + +[[package]] +name = "black" +version = "24.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/f3/465c0eb5cddf7dbbfe1fecd9b875d1dcf51b88923cd2c1d7e9ab95c6336b/black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", size = 1623211 }, + { url = "https://files.pythonhosted.org/packages/df/57/b6d2da7d200773fdfcc224ffb87052cf283cec4d7102fab450b4a05996d8/black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", size = 1457139 }, + { url = "https://files.pythonhosted.org/packages/6e/c5/9023b7673904a5188f9be81f5e129fff69f51f5515655fbd1d5a4e80a47b/black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", size = 1753774 }, + { url = "https://files.pythonhosted.org/packages/e1/32/df7f18bd0e724e0d9748829765455d6643ec847b3f87e77456fc99d0edab/black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e", size = 1414209 }, + { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468 }, + { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270 }, + { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061 }, + { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293 }, + { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256 }, + { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892 }, + { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796 }, + { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986 }, + { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085 }, + { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928 }, + { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875 }, + { url = "https://files.pythonhosted.org/packages/fe/02/f408c804e0ee78c367dcea0a01aedde4f1712af93b8b6e60df981e0228c7/black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd", size = 1622516 }, + { url = "https://files.pythonhosted.org/packages/f8/b9/9b706ed2f55bfb28b436225a9c57da35990c9005b90b8c91f03924454ad7/black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f", size = 1456181 }, + { url = "https://files.pythonhosted.org/packages/0a/1c/314d7f17434a5375682ad097f6f4cc0e3f414f3c95a9b1bb4df14a0f11f9/black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800", size = 1752801 }, + { url = "https://files.pythonhosted.org/packages/39/a7/20e5cd9237d28ad0b31438de5d9f01c8b99814576f4c0cda1edd62caf4b0/black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7", size = 1413626 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898 }, +] + [[package]] name = "cffi" version = "1.17.1" @@ -70,6 +117,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -171,6 +230,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, ] +[[package]] +name = "executing" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/e3/7d45f492c2c4a0e8e0fad57d081a7c8a0286cdd86372b070cca1ec0caa1e/executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab", size = 977485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/fd/afcd0496feca3276f509df3dbd5dae726fcc756f1a08d9e25abe1733f962/executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", size = 25805 }, +] + [[package]] name = "importlib-metadata" version = "8.5.0" @@ -192,6 +260,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] +[[package]] +name = "inline-snapshot" +version = "0.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "black" }, + { name = "click" }, + { name = "executing" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/2d/c4cec2ae99d2d5d3689cd236018a4730bda75f3576e1c5cc04a4cca44dd7/inline_snapshot-0.18.2.tar.gz", hash = "sha256:1be34afa453b75d120e12e8fb4724e28cb23f71b8f0fb0a7bc822a78c97ff826", size = 225777 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/8d/45b249bea2884082360af8cb0c5860a0b9b7ce98291a588067906e70b1aa/inline_snapshot-0.18.2-py3-none-any.whl", hash = "sha256:fd5328a38ab089c565f35abac91bb6bc0bbc54cc7b55b1b2a86a7168146225ca", size = 42325 }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -265,6 +351,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -352,6 +456,7 @@ lint = [ { name = "ruff" }, ] test = [ + { name = "inline-snapshot" }, { name = "pytest" }, { name = "pytest-cov" }, ] @@ -360,6 +465,7 @@ test = [ requires-dist = [ { name = "cffi", specifier = ">=1.17.1" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=8.5.0" }, + { name = "inline-snapshot", marker = "extra == 'test'", specifier = ">=0.18.2" }, { name = "mypy", marker = "extra == 'lint'", specifier = "~=1.11.2" }, { name = "pytest", specifier = ">=3.8" }, { name = "pytest", marker = "extra == 'test'", specifier = "~=7.0" },