diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b778b6..1b97c53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 with: extra_args: --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3705ab9..85050ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,8 +13,8 @@ repos: hooks: - id: mypy - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.5 + rev: v0.11.12 hooks: - - id: ruff + - id: ruff-check args: [--fix] - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index 601bff5..4df558b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ ] [project.optional-dependencies] -lint = ["mypy ~= 1.11.2", "ruff ~= 0.6.5"] +lint = ["mypy ~= 1.11.2", "ruff ~= 0.11.12"] compat = [ "pytest-benchmark ~= 5.0.0", "pytest-xdist ~= 3.6.1", diff --git a/src/pytest_codspeed/config.py b/src/pytest_codspeed/config.py new file mode 100644 index 0000000..0d2d881 --- /dev/null +++ b/src/pytest_codspeed/config.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import pytest + + +@dataclass(frozen=True) +class CodSpeedConfig: + """ + The configuration for the codspeed plugin. + Usually created from the command line arguments. + """ + + warmup_time_ns: int | None = None + max_time_ns: int | None = None + max_rounds: int | None = None + + @classmethod + def from_pytest_config(cls, config: pytest.Config) -> CodSpeedConfig: + warmup_time = config.getoption("--codspeed-warmup-time", None) + warmup_time_ns = ( + int(warmup_time * 1_000_000_000) if warmup_time is not None else None + ) + max_time = config.getoption("--codspeed-max-time", None) + max_time_ns = int(max_time * 1_000_000_000) if max_time is not None else None + return cls( + warmup_time_ns=warmup_time_ns, + max_rounds=config.getoption("--codspeed-max-rounds", None), + max_time_ns=max_time_ns, + ) + + +@dataclass(frozen=True) +class BenchmarkMarkerOptions: + group: str | None = None + """The group name to use for the benchmark.""" + min_time: int | None = None + """ + The minimum time of a round (in seconds). + Only available in walltime mode. + """ + max_time: int | None = None + """ + The maximum time to run the benchmark for (in seconds). + Only available in walltime mode. + """ + max_rounds: int | None = None + """ + The maximum number of rounds to run the benchmark for. + Takes precedence over max_time. Only available in walltime mode. + """ + + @classmethod + def from_pytest_item(cls, item: pytest.Item) -> BenchmarkMarkerOptions: + marker = item.get_closest_marker( + "codspeed_benchmark" + ) or item.get_closest_marker("benchmark") + if marker is None: + return cls() + if len(marker.args) > 0: + raise ValueError( + "Positional arguments are not allowed in the benchmark marker" + ) + + options = cls( + group=marker.kwargs.pop("group", None), + min_time=marker.kwargs.pop("min_time", None), + max_time=marker.kwargs.pop("max_time", None), + max_rounds=marker.kwargs.pop("max_rounds", None), + ) + + if len(marker.kwargs) > 0: + raise ValueError( + "Unknown kwargs passed to benchmark marker: " + + ", ".join(marker.kwargs.keys()) + ) + return options diff --git a/src/pytest_codspeed/instruments/__init__.py b/src/pytest_codspeed/instruments/__init__.py index edd2849..d163783 100644 --- a/src/pytest_codspeed/instruments/__init__.py +++ b/src/pytest_codspeed/instruments/__init__.py @@ -9,6 +9,7 @@ import pytest + from pytest_codspeed.config import BenchmarkMarkerOptions from pytest_codspeed.plugin import CodSpeedConfig T = TypeVar("T") @@ -27,6 +28,7 @@ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: ... @abstractmethod def measure( self, + marker_options: BenchmarkMarkerOptions, name: str, uri: str, fn: Callable[P, T], diff --git a/src/pytest_codspeed/instruments/hooks/__init__.py b/src/pytest_codspeed/instruments/hooks/__init__.py index 65e7fc4..a69489c 100644 --- a/src/pytest_codspeed/instruments/hooks/__init__.py +++ b/src/pytest_codspeed/instruments/hooks/__init__.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from .dist_instrument_hooks import lib as LibType + from .dist_instrument_hooks import InstrumentHooksPointer, LibType SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12) @@ -15,7 +15,7 @@ class InstrumentHooks: """Zig library wrapper class providing benchmark measurement functionality.""" lib: LibType - instance: int + instance: InstrumentHooksPointer def __init__(self) -> None: if os.environ.get("CODSPEED_ENV") is None: @@ -28,17 +28,15 @@ def __init__(self) -> None: from .dist_instrument_hooks import lib # type: ignore except ImportError as e: raise RuntimeError(f"Failed to load instrument hooks library: {e}") from e + self.lib = lib - instance = lib.instrument_hooks_init() - if instance == 0: + self.instance = self.lib.instrument_hooks_init() + if self.instance == 0: raise RuntimeError("Failed to initialize CodSpeed instrumentation library.") if SUPPORTS_PERF_TRAMPOLINE: sys.activate_stack_trampoline("perf") # type: ignore - self.lib = lib - self.instance = instance - def __del__(self): if hasattr(self, "lib") and hasattr(self, "instance"): self.lib.instrument_hooks_deinit(self.instance) diff --git a/src/pytest_codspeed/instruments/hooks/dist_instrument_hooks.pyi b/src/pytest_codspeed/instruments/hooks/dist_instrument_hooks.pyi new file mode 100644 index 0000000..3ae9ec5 --- /dev/null +++ b/src/pytest_codspeed/instruments/hooks/dist_instrument_hooks.pyi @@ -0,0 +1,27 @@ +InstrumentHooksPointer = object + +class lib: + @staticmethod + def instrument_hooks_init() -> InstrumentHooksPointer: ... + @staticmethod + def instrument_hooks_deinit(hooks: InstrumentHooksPointer) -> None: ... + @staticmethod + def instrument_hooks_is_instrumented(hooks: InstrumentHooksPointer) -> bool: ... + @staticmethod + def instrument_hooks_start_benchmark(hooks: InstrumentHooksPointer) -> int: ... + @staticmethod + def instrument_hooks_stop_benchmark(hooks: InstrumentHooksPointer) -> int: ... + @staticmethod + def instrument_hooks_executed_benchmark( + hooks: InstrumentHooksPointer, pid: int, uri: bytes + ) -> int: ... + @staticmethod + def instrument_hooks_set_integration( + hooks: InstrumentHooksPointer, name: bytes, version: bytes + ) -> int: ... + @staticmethod + def callgrind_start_instrumentation() -> int: ... + @staticmethod + def callgrind_stop_instrumentation() -> int: ... + +LibType = type[lib] diff --git a/src/pytest_codspeed/instruments/valgrind.py b/src/pytest_codspeed/instruments/valgrind.py index bed7a56..4476a19 100644 --- a/src/pytest_codspeed/instruments/valgrind.py +++ b/src/pytest_codspeed/instruments/valgrind.py @@ -13,7 +13,7 @@ from pytest import Session from pytest_codspeed.instruments import P, T - from pytest_codspeed.plugin import CodSpeedConfig + from pytest_codspeed.plugin import BenchmarkMarkerOptions, CodSpeedConfig SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12) @@ -35,7 +35,7 @@ def __init__(self, config: CodSpeedConfig) -> None: def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: config = ( f"mode: instrumentation, " - f"callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'}" + f"callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'}" ) warnings = [] if not self.should_measure: @@ -49,6 +49,7 @@ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: def measure( self, + marker_options: BenchmarkMarkerOptions, name: str, uri: str, fn: Callable[P, T], diff --git a/src/pytest_codspeed/instruments/walltime.py b/src/pytest_codspeed/instruments/walltime.py index ab7a0cc..28f2412 100644 --- a/src/pytest_codspeed/instruments/walltime.py +++ b/src/pytest_codspeed/instruments/walltime.py @@ -23,12 +23,12 @@ from pytest import Session from pytest_codspeed.instruments import P, T - from pytest_codspeed.plugin import CodSpeedConfig + from pytest_codspeed.plugin import BenchmarkMarkerOptions, CodSpeedConfig DEFAULT_WARMUP_TIME_NS = 1_000_000_000 DEFAULT_MAX_TIME_NS = 3_000_000_000 TIMER_RESOLUTION_NS = get_clock_info("perf_counter").resolution * 1e9 -DEFAULT_MIN_ROUND_TIME_NS = TIMER_RESOLUTION_NS * 1_000_000 +DEFAULT_MIN_ROUND_TIME_NS = int(TIMER_RESOLUTION_NS * 1_000_000) IQR_OUTLIER_FACTOR = 1.5 STDEV_OUTLIER_FACTOR = 3 @@ -42,16 +42,35 @@ class BenchmarkConfig: max_rounds: int | None @classmethod - def from_codspeed_config(cls, config: CodSpeedConfig) -> BenchmarkConfig: + def from_codspeed_config_and_marker_data( + cls, config: CodSpeedConfig, marker_data: BenchmarkMarkerOptions + ) -> BenchmarkConfig: + if marker_data.max_time is not None: + max_time_ns = int(marker_data.max_time * 1e9) + elif config.max_time_ns is not None: + max_time_ns = config.max_time_ns + else: + max_time_ns = DEFAULT_MAX_TIME_NS + + if marker_data.max_rounds is not None: + max_rounds = marker_data.max_rounds + elif config.max_rounds is not None: + max_rounds = config.max_rounds + else: + max_rounds = None + + if marker_data.min_time is not None: + min_round_time_ns = int(marker_data.min_time * 1e9) + else: + min_round_time_ns = DEFAULT_MIN_ROUND_TIME_NS + return cls( warmup_time_ns=config.warmup_time_ns if config.warmup_time_ns is not None else DEFAULT_WARMUP_TIME_NS, - min_round_time_ns=DEFAULT_MIN_ROUND_TIME_NS, - max_time_ns=config.max_time_ns - if config.max_time_ns is not None - else DEFAULT_MAX_TIME_NS, - max_rounds=config.max_rounds, + min_round_time_ns=min_round_time_ns, + max_time_ns=max_time_ns, + max_rounds=max_rounds, ) @@ -231,6 +250,7 @@ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: def measure( self, + marker_options: BenchmarkMarkerOptions, name: str, uri: str, fn: Callable[P, T], @@ -244,7 +264,9 @@ def measure( fn=fn, args=args, kwargs=kwargs, - config=BenchmarkConfig.from_codspeed_config(self.config), + config=BenchmarkConfig.from_codspeed_config_and_marker_data( + self.config, marker_options + ), ) self.benchmarks.append(bench) return out diff --git a/src/pytest_codspeed/plugin.py b/src/pytest_codspeed/plugin.py index 17374f2..f3dfdc2 100644 --- a/src/pytest_codspeed/plugin.py +++ b/src/pytest_codspeed/plugin.py @@ -14,6 +14,7 @@ import pytest from _pytest.fixtures import FixtureManager +from pytest_codspeed.config import BenchmarkMarkerOptions, CodSpeedConfig from pytest_codspeed.instruments import ( MeasurementMode, get_instrument_from_mode, @@ -58,8 +59,7 @@ def pytest_addoption(parser: pytest.Parser): action="store", type=float, help=( - "The time to warm up the benchmark for (in seconds), " - "only for walltime mode" + "The time to warm up the benchmark for (in seconds), only for walltime mode" ), ) group.addoption( @@ -82,27 +82,6 @@ def pytest_addoption(parser: pytest.Parser): ) -@dataclass(frozen=True) -class CodSpeedConfig: - warmup_time_ns: int | None = None - max_time_ns: int | None = None - max_rounds: int | None = None - - @classmethod - def from_pytest_config(cls, config: pytest.Config) -> CodSpeedConfig: - warmup_time = config.getoption("--codspeed-warmup-time", None) - warmup_time_ns = ( - int(warmup_time * 1_000_000_000) if warmup_time is not None else None - ) - max_time = config.getoption("--codspeed-max-time", None) - max_time_ns = int(max_time * 1_000_000_000) if max_time is not None else None - return cls( - warmup_time_ns=warmup_time_ns, - max_rounds=config.getoption("--codspeed-max-rounds", None), - max_time_ns=max_time_ns, - ) - - @dataclass(unsafe_hash=True) class CodSpeedPlugin: is_codspeed_enabled: bool @@ -254,20 +233,21 @@ def pytest_collection_modifyitems( def _measure( plugin: CodSpeedPlugin, - nodeid: str, + node: pytest.Item, config: pytest.Config, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs, ) -> T: + marker_options = BenchmarkMarkerOptions.from_pytest_item(node) random.seed(0) is_gc_enabled = gc.isenabled() if is_gc_enabled: 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) + uri, name = get_git_relative_uri_and_name(node.nodeid, config.rootpath) + return plugin.instrument.measure(marker_options, name, uri, fn, *args, **kwargs) finally: # Ensure GC is re-enabled even if the test failed if is_gc_enabled: @@ -276,13 +256,13 @@ def _measure( def wrap_runtest( plugin: CodSpeedPlugin, - nodeid: str, + node: pytest.Item, config: pytest.Config, fn: Callable[P, T], ) -> Callable[P, T]: @functools.wraps(fn) def wrapped(*args: P.args, **kwargs: P.kwargs) -> T: - return _measure(plugin, nodeid, config, fn, *args, **kwargs) + return _measure(plugin, node, config, fn, *args, **kwargs) return wrapped @@ -299,7 +279,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, item.config, item.runtest) return None @@ -343,9 +323,7 @@ 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 - ) + return _measure(plugin, self._request.node, config, func, *args, **kwargs) else: return func(*args, **kwargs) diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py index 1a6155e..fa19362 100644 --- a/tests/test_pytest_plugin.py +++ b/tests/test_pytest_plugin.py @@ -220,6 +220,46 @@ def _(): ) +def test_codspeed_marker_unexpected_args(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.codspeed_benchmark( + "positional_arg" + ) + def test_bench(): + pass + """ + ) + result = pytester.runpytest("--codspeed") + assert result.ret == 1 + result.stdout.fnmatch_lines_random( + ["*ValueError: Positional arguments are not allowed in the benchmark marker*"], + ) + + +def test_codspeed_marker_unexpected_kwargs(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.codspeed_benchmark( + not_allowed=True + ) + def test_bench(): + pass + """ + ) + result = pytester.runpytest("--codspeed") + assert result.ret == 1 + result.stdout.fnmatch_lines_random( + [ + "*ValueError: Unknown kwargs passed to benchmark marker: not_allowed*", + ], + ) + + def test_pytest_benchmark_extra_info(pytester: pytest.Pytester) -> None: """https://pytest-benchmark.readthedocs.io/en/latest/usage.html#extra-info""" pytester.makepyfile( diff --git a/tests/test_pytest_plugin_cpu_instrumentation.py b/tests/test_pytest_plugin_cpu_instrumentation.py index e911c52..d72ca4a 100644 --- a/tests/test_pytest_plugin_cpu_instrumentation.py +++ b/tests/test_pytest_plugin_cpu_instrumentation.py @@ -79,12 +79,12 @@ def fixtured_child(): "py::ValgrindInstrument.measure..__codspeed_root_frame__" in line for line in lines ), "No root frame found in perf map" - assert any( - "py::test_some_addition_marked" in line for line in lines - ), "No marked test frame found in perf map" - assert any( - "py::test_some_addition_fixtured" in line for line in lines - ), "No fixtured test frame found in perf map" + assert any("py::test_some_addition_marked" in line for line in lines), ( + "No marked test frame found in perf map" + ) + assert any("py::test_some_addition_fixtured" in line for line in lines), ( + "No fixtured test frame found in perf map" + ) assert any( "py::test_some_addition_fixtured..fixtured_child" in line for line in lines diff --git a/uv.lock b/uv.lock index 0b40df2..0a56fed 100644 --- a/uv.lock +++ b/uv.lock @@ -371,7 +371,7 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'test'", specifier = "~=4.0.0" }, { name = "pytest-xdist", marker = "extra == 'compat'", specifier = "~=3.6.1" }, { name = "rich", specifier = ">=13.8.1" }, - { name = "ruff", marker = "extra == 'lint'", specifier = "~=0.6.5" }, + { name = "ruff", marker = "extra == 'lint'", specifier = "~=0.11.12" }, ] [package.metadata.requires-dev] @@ -419,27 +419,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.6.9" +version = "0.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/0d/6148a48dab5662ca1d5a93b7c0d13c03abd3cc7e2f35db08410e47cef15d/ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2", size = 3095355 } +sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/8f/f7a0a0ef1818662efb32ed6df16078c95da7a0a3248d64c2410c1e27799f/ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd", size = 10440526 }, - { url = "https://files.pythonhosted.org/packages/8b/69/b179a5faf936a9e2ab45bb412a668e4661eded964ccfa19d533f29463ef6/ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec", size = 10034612 }, - { url = "https://files.pythonhosted.org/packages/c7/ef/fd1b4be979c579d191eeac37b5cfc0ec906de72c8bcd8595e2c81bb700c1/ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c", size = 9706197 }, - { url = "https://files.pythonhosted.org/packages/29/61/b376d775deb5851cb48d893c568b511a6d3625ef2c129ad5698b64fb523c/ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e", size = 10751855 }, - { url = "https://files.pythonhosted.org/packages/13/d7/def9e5f446d75b9a9c19b24231a3a658c075d79163b08582e56fa5dcfa38/ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577", size = 10200889 }, - { url = "https://files.pythonhosted.org/packages/6c/d6/7f34160818bcb6e84ce293a5966cba368d9112ff0289b273fbb689046047/ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829", size = 11038678 }, - { url = "https://files.pythonhosted.org/packages/13/34/a40ff8ae62fb1b26fb8e6fa7e64bc0e0a834b47317880de22edd6bfb54fb/ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5", size = 11808682 }, - { url = "https://files.pythonhosted.org/packages/2e/6d/25a4386ae4009fc798bd10ba48c942d1b0b3e459b5403028f1214b6dd161/ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7", size = 11330446 }, - { url = "https://files.pythonhosted.org/packages/f7/f6/bdf891a9200d692c94ebcd06ae5a2fa5894e522f2c66c2a12dd5d8cb2654/ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f", size = 12483048 }, - { url = "https://files.pythonhosted.org/packages/a7/86/96f4252f41840e325b3fa6c48297e661abb9f564bd7dcc0572398c8daa42/ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa", size = 10936855 }, - { url = "https://files.pythonhosted.org/packages/45/87/801a52d26c8dbf73424238e9908b9ceac430d903c8ef35eab1b44fcfa2bd/ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb", size = 10713007 }, - { url = "https://files.pythonhosted.org/packages/be/27/6f7161d90320a389695e32b6ebdbfbedde28ccbf52451e4b723d7ce744ad/ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0", size = 10274594 }, - { url = "https://files.pythonhosted.org/packages/00/52/dc311775e7b5f5b19831563cb1572ecce63e62681bccc609867711fae317/ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625", size = 10608024 }, - { url = "https://files.pythonhosted.org/packages/98/b6/be0a1ddcbac65a30c985cf7224c4fce786ba2c51e7efeb5178fe410ed3cf/ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039", size = 10982085 }, - { url = "https://files.pythonhosted.org/packages/bb/a4/c84bc13d0b573cf7bb7d17b16d6d29f84267c92d79b2f478d4ce322e8e72/ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d", size = 8522088 }, - { url = "https://files.pythonhosted.org/packages/74/be/fc352bd8ca40daae8740b54c1c3e905a7efe470d420a268cd62150248c91/ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117", size = 9359275 }, - { url = "https://files.pythonhosted.org/packages/3e/14/fd026bc74ded05e2351681545a5f626e78ef831f8edce064d61acd2e6ec7/ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93", size = 8679879 }, + { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511 }, + { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573 }, + { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770 }, ] [[package]]