From 18543081dac24f7f4e9e1463b41bbbfd5bf88d09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:17:25 +0000 Subject: [PATCH 1/3] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.9 → v0.16.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.9...v0.16.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0daf9719..4038dab0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: no-commit-to-branch args: [--branch, develop, --branch, main] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.15.9" + rev: "v0.16.2" hooks: - id: ruff-check args: ["--fix"] From f01150d371cb04ab414f6792b3ebb75b38dd7d57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:17:40 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/ruff_sync/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ruff_sync/constants.py b/src/ruff_sync/constants.py index 872c0738..99a58d82 100644 --- a/src/ruff_sync/constants.py +++ b/src/ruff_sync/constants.py @@ -129,7 +129,7 @@ def get_canonical(cls, key: str) -> str: def resolve_defaults( branch: str | MissingType, - path: str | None | MissingType, + path: str | MissingType | None, exclude: Iterable[str] | MissingType, ) -> tuple[str, str | None, Iterable[str]]: """Resolve MISSING sentinel values to their effective defaults. From 595e29988c6a7273dbff2ebb9cb5fab08c122420 Mon Sep 17 00:00:00 2001 From: Gabriel G Date: Sat, 15 Aug 2026 09:02:06 -0400 Subject: [PATCH 3/3] chore(deps): sync ruff dependency with pre-commit v0.16.3 and handle PLR0917 --- .agents/DEPENDENCIES.md | 2 + .agents/args_refactor_plan.md | 22 ++++++---- .agents/cli_animation_plan.md | 4 +- .agents/formatters-architecture.md | 13 +++--- .agents/gitlab-reports.md | 42 ++++++++++++------- .agents/issue-102-context.md | 14 ++++++- .agents/plans/issue-116-config-validation.md | 39 +++++++++-------- .agents/skills/dirty-equals/SKILL.md | 11 +++-- .../references/common-matchers.md | 12 +++--- .../dirty-equals/references/toml-matching.md | 14 ++++--- .agents/skills/textual/SKILL.md | 3 ++ .agents/skills/textual/references/events.md | 3 ++ .agents/skills/textual/references/testing.md | 1 + .agents/skills/textual/references/widgets.md | 3 ++ .../references/advanced-narrowing.md | 4 ++ .../references/error-code-lookup.md | 16 ++++--- .../type-checking/references/generics.md | 6 ++- .../skills/type-checking/references/naming.md | 2 + .../references/protocol-patterns.md | 9 +++- .../references/refactoring-patterns.md | 4 ++ .agents/skills/warnings-control/SKILL.md | 5 +++ .agents/tui_design.md | 4 +- .agents/workflows/update-screenshots.md | 2 +- .pre-commit-config.yaml | 2 +- pyproject.toml | 1 + tests/ruff.toml | 2 + uv.lock | 42 +++++++++---------- 27 files changed, 179 insertions(+), 103 deletions(-) diff --git a/.agents/DEPENDENCIES.md b/.agents/DEPENDENCIES.md index 2b031180..e7df8fc2 100644 --- a/.agents/DEPENDENCIES.md +++ b/.agents/DEPENDENCIES.md @@ -29,10 +29,12 @@ Never import optional dependencies at the top level of a module. All imports mus def run_tui_feature(): # 1. First, check availability (fast, lightweight) from ruff_sync.dependencies import require_dependency + require_dependency("textual", extra_name="tui") # 2. Then, perform local import (delayed expensive cycle) from textual.app import App + ... ``` diff --git a/.agents/args_refactor_plan.md b/.agents/args_refactor_plan.md index ddd3dfc1..c5ba02d8 100644 --- a/.agents/args_refactor_plan.md +++ b/.agents/args_refactor_plan.md @@ -96,11 +96,11 @@ class ExecutionArgs(NamedTuple): semantic: bool diff: bool init: bool - pre_commit: bool # plain bool — MISSING resolved to default + pre_commit: bool # plain bool — MISSING resolved to default save: bool | None output_format: OutputFormat - validate: bool # plain bool — MISSING resolved to default - strict: bool # plain bool — MISSING resolved to default + validate: bool # plain bool — MISSING resolved to default + strict: bool # plain bool — MISSING resolved to default ``` > [!NOTE] @@ -120,9 +120,9 @@ class Arguments(NamedTuple): def resolve(self) -> ExecutionArgs: """Resolve all MISSING sentinels to their effective defaults for execution.""" _, _, _, eff_validate, eff_strict, eff_pre_commit = resolve_defaults( - MISSING, # branch — already resolved, pass MISSING to skip - MISSING, # path — already resolved, pass MISSING to skip - MISSING, # exclude — already resolved, pass MISSING to skip + MISSING, # branch — already resolved, pass MISSING to skip + MISSING, # path — already resolved, pass MISSING to skip + MISSING, # exclude — already resolved, pass MISSING to skip self.validate, self.strict, self.pre_commit, @@ -205,11 +205,14 @@ Replace the `resolve_defaults()` call in `Arguments.resolve()` with `resolve_boo ```python from ruff_sync.constants import resolve_bool_flags + class Arguments(NamedTuple): # ... def resolve(self) -> ExecutionArgs: eff_validate, eff_strict, eff_pre_commit = resolve_bool_flags( - self.validate, self.strict, self.pre_commit, + self.validate, + self.strict, + self.pre_commit, ) return ExecutionArgs( command=self.command, @@ -291,7 +294,7 @@ call is actually redundant. However, if the function is also used by code that p async def _merge_multiple_upstreams( target_doc: TOMLDocument, is_target_ruff_toml: bool, - args: ExecutionArgs, # ← changed from Arguments + args: ExecutionArgs, # ← changed from Arguments client: httpx.AsyncClient, ) -> TOMLDocument: # No resolve_defaults() needed — args already has plain values @@ -363,17 +366,20 @@ Add **new** tests for `resolve_bool_flags()`: ```python from ruff_sync.constants import resolve_bool_flags + def test_resolve_bool_flags_all_missing(): validate, strict, pre_commit = resolve_bool_flags(MISSING, MISSING, MISSING) assert validate is False assert strict is False assert pre_commit is True + def test_resolve_bool_flags_strict_implies_validate(): validate, strict, pre_commit = resolve_bool_flags(MISSING, True, MISSING) assert validate is True assert strict is True + def test_resolve_bool_flags_explicit_false(): validate, strict, pre_commit = resolve_bool_flags(False, False, False) assert validate is False diff --git a/.agents/cli_animation_plan.md b/.agents/cli_animation_plan.md index cfcb306e..c0fc3c7a 100644 --- a/.agents/cli_animation_plan.md +++ b/.agents/cli_animation_plan.md @@ -441,9 +441,7 @@ def recordings(ctx, tape=None): tape_files = [tape_file] else: # Process all tape files except _common.tape - tape_files = sorted( - f for f in tapes_dir.glob("*.tape") if not f.name.startswith("_") - ) + tape_files = sorted(f for f in tapes_dir.glob("*.tape") if not f.name.startswith("_")) if not tape_files: print("⚠️ No tape files found in tapes/") diff --git a/.agents/formatters-architecture.md b/.agents/formatters-architecture.md index 38ab408d..4f27ae6e 100644 --- a/.agents/formatters-architecture.md +++ b/.agents/formatters-architecture.md @@ -58,8 +58,8 @@ def error( message: str, file_path: pathlib.Path | None = None, logger: logging.Logger | None = None, - check_name: str = "ruff-sync/config-drift", # machine-readable rule ID - drift_key: str | None = None, # e.g. "lint.select" + check_name: str = "ruff-sync/config-drift", # machine-readable rule ID + drift_key: str | None = None, # e.g. "lint.select" ) -> None: ... ``` @@ -107,11 +107,11 @@ this even when `UpstreamError` or another exception occurs. 1. **Add the format value** to `OutputFormat` in `src/ruff_sync/constants.py`: ```python class OutputFormat(str, enum.Enum): - TEXT = "text" - JSON = "json" + TEXT = "text" + JSON = "json" GITHUB = "github" GITLAB = "gitlab" - SARIF = "sarif" # new + SARIF = "sarif" # new ``` 2. **Implement the class** in `src/ruff_sync/formatters.py`. For a @@ -151,7 +151,7 @@ fmt = get_formatter(args.output_format) try: ... finally: - fmt.finalize() # no-op for streaming; flushes JSON for accumulating + fmt.finalize() # no-op for streaming; flushes JSON for accumulating ``` `finalize()` is always called unconditionally — **do not** guard it with @@ -168,6 +168,7 @@ is newly introduced or already resolved between branches. ```python import hashlib + def _make_fingerprint(upstream_url: str, local_file: str, drift_key: str | None) -> str: if drift_key: raw = f"ruff-sync:drift:{upstream_url}:{local_file}:{drift_key}" diff --git a/.agents/gitlab-reports.md b/.agents/gitlab-reports.md index 2fcc86c7..bd81ae7a 100644 --- a/.agents/gitlab-reports.md +++ b/.agents/gitlab-reports.md @@ -145,6 +145,7 @@ For a "config drift" issue per key (e.g., `lint.select`), a good stable fingerpr ```python import hashlib + def make_fingerprint(upstream_url: str, local_file: str, drift_key: str) -> str: """Stable fingerprint for a config drift issue.""" raw = f"ruff-sync:drift:{upstream_url}:{local_file}:{drift_key}" @@ -281,13 +282,15 @@ class GitlabFormatter: fingerprint: str | None = None, ) -> None: (logger or LOGGER).error(message) - self._issues.append(self._make_issue( - description=message, - check_name=check_name, - severity="major", - file_path=file_path, - fingerprint=fingerprint, - )) + self._issues.append( + self._make_issue( + description=message, + check_name=check_name, + severity="major", + file_path=file_path, + fingerprint=fingerprint, + ) + ) def warning( self, @@ -298,13 +301,15 @@ class GitlabFormatter: fingerprint: str | None = None, ) -> None: (logger or LOGGER).warning(message) - self._issues.append(self._make_issue( - description=message, - check_name=check_name, - severity="minor", - file_path=file_path, - fingerprint=fingerprint, - )) + self._issues.append( + self._make_issue( + description=message, + check_name=check_name, + severity="minor", + file_path=file_path, + fingerprint=fingerprint, + ) + ) def _make_issue( self, @@ -327,6 +332,7 @@ class GitlabFormatter: @staticmethod def _auto_fingerprint(description: str, path: str) -> str: import hashlib + raw = f"ruff-sync:drift:{path}:{description}" return hashlib.md5(raw.encode()).hexdigest() @@ -370,7 +376,7 @@ class OutputFormat(str, enum.Enum): TEXT = "text" JSON = "json" GITHUB = "github" - GITLAB = "gitlab" # NEW + GITLAB = "gitlab" # NEW ``` Update `get_formatter` in `formatters.py`: @@ -530,7 +536,7 @@ fmt = get_formatter(args.output_format) try: ... finally: - fmt.finalize() # Writes the JSON array to stdout (piped to file by CI) + fmt.finalize() # Writes the JSON array to stdout (piped to file by CI) ``` **Do not** guard the call with `isinstance` or `hasattr` checks. @@ -562,6 +568,7 @@ from unittest.mock import patch from ruff_sync.formatters import GitlabFormatter + def test_gitlab_formatter_empty_on_no_issues(capsys): fmt = GitlabFormatter() fmt.finalize() @@ -569,6 +576,7 @@ def test_gitlab_formatter_empty_on_no_issues(capsys): issues = json.loads(captured.out) assert issues == [] + def test_gitlab_formatter_error_produces_major_issue(capsys): fmt = GitlabFormatter() fmt.error("drift found", file_path=pathlib.Path("pyproject.toml")) @@ -581,6 +589,7 @@ def test_gitlab_formatter_error_produces_major_issue(capsys): assert issues[0]["location"]["lines"]["begin"] == 1 assert "fingerprint" in issues[0] + def test_gitlab_formatter_fingerprint_is_stable(capsys): fmt1 = GitlabFormatter() fmt2 = GitlabFormatter() @@ -594,6 +603,7 @@ def test_gitlab_formatter_fingerprint_is_stable(capsys): issues2 = json.loads(out2) assert issues1[0]["fingerprint"] == issues2[0]["fingerprint"] + def test_gitlab_formatter_no_bom(capsys): fmt = GitlabFormatter() fmt.finalize() diff --git a/.agents/issue-102-context.md b/.agents/issue-102-context.md index e33b178e..a69ec3fa 100644 --- a/.agents/issue-102-context.md +++ b/.agents/issue-102-context.md @@ -17,8 +17,18 @@ class ResultFormatter(Protocol): def note(self, message: str) -> None: ... def info(self, message: str, logger: logging.Logger | None = None) -> None: ... def success(self, message: str) -> None: ... - def error(self, message: str, file_path: pathlib.Path | None = None, logger: logging.Logger | None = None) -> None: ... - def warning(self, message: str, file_path: pathlib.Path | None = None, logger: logging.Logger | None = None) -> None: ... + def error( + self, + message: str, + file_path: pathlib.Path | None = None, + logger: logging.Logger | None = None, + ) -> None: ... + def warning( + self, + message: str, + file_path: pathlib.Path | None = None, + logger: logging.Logger | None = None, + ) -> None: ... def debug(self, message: str, logger: logging.Logger | None = None) -> None: ... def diff(self, diff_text: str) -> None: ... ``` diff --git a/.agents/plans/issue-116-config-validation.md b/.agents/plans/issue-116-config-validation.md index cd395faa..00befd03 100644 --- a/.agents/plans/issue-116-config-validation.md +++ b/.agents/plans/issue-116-config-validation.md @@ -88,6 +88,7 @@ def validate_toml_syntax(doc: TOMLDocument) -> bool: """ try: import tomlkit # already a dep + tomlkit.parse(doc.as_string()) return True except Exception: # noqa: BLE001 @@ -146,9 +147,7 @@ def validate_ruff_accepts_config(doc: TOMLDocument, is_ruff_toml: bool = False) ) return False except FileNotFoundError: - LOGGER.warning( - "⚠️ `ruff` not found on PATH — skipping Ruff config validation." - ) + LOGGER.warning("⚠️ `ruff` not found on PATH — skipping Ruff config validation.") return True # Soft fail: don't block if ruff isn't installed except subprocess.TimeoutExpired: LOGGER.warning("⚠️ Ruff config validation timed out — skipping.") @@ -184,7 +183,7 @@ Validation is **opt-in**. Before touching `core.py`, wire up the CLI flag. class Arguments(NamedTuple): ... validate: bool = False # run --validate checks before writing - strict: bool = False # treat warnings as errors (implies validate) + strict: bool = False # treat warnings as errors (implies validate) ``` **In `common_parser`** (around line 236), add: @@ -230,18 +229,17 @@ Open `core.py`. Find the `pull()` function (around line 1103). Look for this blo **After** this block (and **before** `should_save = args.save ...`), insert: ```python - # Validation is opt-in — only run if --validate (or --strict) was passed - if args.validate: - is_ruff_toml = is_ruff_toml_file(_source_toml_path.name) - from ruff_sync.validation import validate_merged_config # noqa: PLC0415 - if not validate_merged_config( - source_doc, is_ruff_toml=is_ruff_toml, strict=args.strict - ): - fmt.error( - "❌ Merged config failed validation. Local file left unchanged.", - logger=LOGGER, - ) - return 1 +# Validation is opt-in — only run if --validate (or --strict) was passed +if args.validate: + is_ruff_toml = is_ruff_toml_file(_source_toml_path.name) + from ruff_sync.validation import validate_merged_config # noqa: PLC0415 + + if not validate_merged_config(source_doc, is_ruff_toml=is_ruff_toml, strict=args.strict): + fmt.error( + "❌ Merged config failed validation. Local file left unchanged.", + logger=LOGGER, + ) + return 1 ``` > **Note**: The inline import avoids a circular import issue if `validation.py` ever needs to @@ -437,6 +435,7 @@ Add to `tests/test_config_validation.py`: ```python def test_version_consistency_warn_on_mismatch(caplog: pytest.LogCaptureFixture) -> None: import logging + doc = tomlkit.parse( '[project]\nrequires-python = ">=3.10"\n\n[tool.ruff]\ntarget-version = "py39"\n' ) @@ -492,9 +491,7 @@ def _get_deprecated_rule_codes() -> frozenset[str]: if result.returncode != 0: return frozenset() rules = json.loads(result.stdout) - return frozenset( - r["code"] for r in rules if r.get("deprecated") is True - ) + return frozenset(r["code"] for r in rules if r.get("deprecated") is True) except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError, KeyError): return frozenset() ``` @@ -573,7 +570,9 @@ def check_deprecated_rules( is_ruff_toml: bool = False, _deprecated_codes: frozenset[str] | None = None, ) -> None: - deprecated_codes = _deprecated_codes if _deprecated_codes is not None else _get_deprecated_rule_codes() + deprecated_codes = ( + _deprecated_codes if _deprecated_codes is not None else _get_deprecated_rule_codes() + ) ... ``` diff --git a/.agents/skills/dirty-equals/SKILL.md b/.agents/skills/dirty-equals/SKILL.md index fa13c740..b7577bb8 100644 --- a/.agents/skills/dirty-equals/SKILL.md +++ b/.agents/skills/dirty-equals/SKILL.md @@ -14,13 +14,16 @@ Instead of asserting on every field manually, compare against a "dirty" object t ```python from dirty_equals import IsInt, IsPartialDict, IsStr + def test_config_logic(): result = {"status": "active", "version": 1, "extra": "data"} # Declarative assertion - assert result == IsPartialDict({ - "status": IsStr(regex="act.*"), - "version": IsInt(gt=0), - }) + assert result == IsPartialDict( + { + "status": IsStr(regex="act.*"), + "version": IsInt(gt=0), + } + ) ``` ## Detailed reference diff --git a/.agents/skills/dirty-equals/references/common-matchers.md b/.agents/skills/dirty-equals/references/common-matchers.md index 37a7bf1c..dfad7e29 100644 --- a/.agents/skills/dirty-equals/references/common-matchers.md +++ b/.agents/skills/dirty-equals/references/common-matchers.md @@ -24,11 +24,13 @@ import httpx import pathlib # Match a partial dict with mixed types -assert response_data == IsPartialDict({ - "url": IsInstance(httpx.URL), - "status": "success", - "retries": 0, -}) +assert response_data == IsPartialDict( + { + "url": IsInstance(httpx.URL), + "status": "success", + "retries": 0, + } +) ``` ### String and Path Matching diff --git a/.agents/skills/dirty-equals/references/toml-matching.md b/.agents/skills/dirty-equals/references/toml-matching.md index 3630a861..28d97c57 100644 --- a/.agents/skills/dirty-equals/references/toml-matching.md +++ b/.agents/skills/dirty-equals/references/toml-matching.md @@ -13,7 +13,7 @@ from dirty_equals import IsPartialDict import tomlkit # Parse some TOML -doc = tomlkit.parse('[tool.ruff]\nline-length = 80') +doc = tomlkit.parse("[tool.ruff]\nline-length = 80") # Match the tool.ruff section ruff_config = doc["tool"]["ruff"] @@ -46,9 +46,11 @@ args = ruff_sync_cli.Arguments( ) # Convert to dict and match specific fields -assert args._asdict() == IsPartialDict({ - "command": "pull", - "upstream": (IsInstance(httpx.URL),), - "to": IsInstance(pathlib.Path), -}) +assert args._asdict() == IsPartialDict( + { + "command": "pull", + "upstream": (IsInstance(httpx.URL),), + "to": IsInstance(pathlib.Path), + } +) ``` diff --git a/.agents/skills/textual/SKILL.md b/.agents/skills/textual/SKILL.md index 511c5c7d..b8965e8e 100644 --- a/.agents/skills/textual/SKILL.md +++ b/.agents/skills/textual/SKILL.md @@ -14,8 +14,10 @@ from __future__ import annotations from textual.app import App, ComposeResult from textual.widgets import Header, Footer, Static + class SimpleApp(App[None]): """A minimal Textual app.""" + BINDINGS = [("q", "quit", "Quit")] def compose(self) -> ComposeResult: @@ -23,6 +25,7 @@ class SimpleApp(App[None]): yield Static("Hello, [bold blue]Textual[/bold blue]!") yield Footer() + if __name__ == "__main__": SimpleApp().run() ``` diff --git a/.agents/skills/textual/references/events.md b/.agents/skills/textual/references/events.md index db827756..9fa12842 100644 --- a/.agents/skills/textual/references/events.md +++ b/.agents/skills/textual/references/events.md @@ -33,6 +33,7 @@ Define `reactive` attributes to automatically trigger updates. Use `watch_ None: self.data = data diff --git a/.agents/skills/textual/references/testing.md b/.agents/skills/textual/references/testing.md index e2a19ccd..4cf60363 100644 --- a/.agents/skills/textual/references/testing.md +++ b/.agents/skills/textual/references/testing.md @@ -10,6 +10,7 @@ Use the `pilot` object to interact with your app in a headless state. import pytest from my_app import SimpleApp + @pytest.mark.asyncio async def test_button_click(): app = SimpleApp() diff --git a/.agents/skills/textual/references/widgets.md b/.agents/skills/textual/references/widgets.md index 5005540d..70b78f35 100644 --- a/.agents/skills/textual/references/widgets.md +++ b/.agents/skills/textual/references/widgets.md @@ -82,6 +82,7 @@ Modals are screens with a transparent or dim background that overlay the main ap from textual.screen import ModalScreen from textual.app import App + class OmniboxScreen(ModalScreen[str]): # A modal screen that returns a `str` when dismissed. def compose(self) -> ComposeResult: @@ -92,11 +93,13 @@ class OmniboxScreen(ModalScreen[str]): def on_input_submitted(self, event: Input.Submitted) -> None: self.dismiss(event.value) + # In the main App or Screen: def on_key(self, event: events.Key) -> None: if event.key == "ctrl+p": self.push_screen(OmniboxScreen(), self.handle_omnibox_result) + def handle_omnibox_result(self, result: str | None) -> None: if result: print(f"Selected: {result}") diff --git a/.agents/skills/type-checking/references/advanced-narrowing.md b/.agents/skills/type-checking/references/advanced-narrowing.md index 56665a22..17cbf214 100644 --- a/.agents/skills/type-checking/references/advanced-narrowing.md +++ b/.agents/skills/type-checking/references/advanced-narrowing.md @@ -19,9 +19,11 @@ Use `TypeIs` when you want to definitively split a Union. from typing import Union from typing_extensions import TypeIs + def is_str(val: Union[str, int]) -> TypeIs[str]: return isinstance(val, str) + def process(val: Union[str, int]) -> None: if is_str(val): # Mypy knows val is str @@ -39,10 +41,12 @@ Use `@runtime_checkable Protocol` with `TypeIs` for structural subtyping. from typing import Protocol, runtime_checkable from typing_extensions import TypeIs + @runtime_checkable class Reader(Protocol): def read(self) -> str: ... + def is_reader(val: object) -> TypeIs[Reader]: return isinstance(val, Reader) ``` diff --git a/.agents/skills/type-checking/references/error-code-lookup.md b/.agents/skills/type-checking/references/error-code-lookup.md index e307d3ae..747b6c40 100644 --- a/.agents/skills/type-checking/references/error-code-lookup.md +++ b/.agents/skills/type-checking/references/error-code-lookup.md @@ -17,8 +17,8 @@ Match the Mypy error code from your terminal to the project-safe resolution patt **Bad (Cast)**: ```python -val = get_union() # str | None -val.upper() # [union-attr] "None" has no attribute "upper" +val = get_union() # str | None +val.upper() # [union-attr] "None" has no attribute "upper" # NO: val = cast(str, val) ``` @@ -26,7 +26,7 @@ val.upper() # [union-attr] "None" has no attribute "upper" ```python val = get_union() if val is not None: - val.upper() # FIXED + val.upper() # FIXED ``` ### Fixing `[assignment]` (Dealing with `tomlkit` or `Any`) @@ -34,7 +34,7 @@ if val is not None: **Bad (Cast)**: ```python doc = tomlkit.parse(...) -tool: Table = doc["tool"] # [assignment] Incompatible types (Item vs Table) +tool: Table = doc["tool"] # [assignment] Incompatible types (Item vs Table) # NO: tool = cast(Table, doc["tool"]) ``` @@ -52,8 +52,10 @@ if not isinstance(tool, Table): **Bad (Cast)**: ```python def process_str(s: str): ... + + data: Union[str, int] = ... -process_str(data) # [arg-type] Argument 1 has incompatible type +process_str(data) # [arg-type] Argument 1 has incompatible type # NO: process_str(cast(str, data)) ``` @@ -61,9 +63,11 @@ process_str(data) # [arg-type] Argument 1 has incompatible type ```python from typing_extensions import TypeIs + def is_str(v: object) -> TypeIs[str]: return isinstance(v, str) + if is_str(data): - process_str(data) # FIXED + process_str(data) # FIXED ``` diff --git a/.agents/skills/type-checking/references/generics.md b/.agents/skills/type-checking/references/generics.md index a5498c8d..d1086b05 100644 --- a/.agents/skills/type-checking/references/generics.md +++ b/.agents/skills/type-checking/references/generics.md @@ -21,6 +21,7 @@ from typing import TypeVar, Protocol, Generic, Sequence T = TypeVar("T_co", covariant=True) + class Producer(Generic[T]): def __init__(self, items: Sequence[T]) -> None: self._items = items @@ -39,17 +40,20 @@ from typing import TypeVar, Union # T must be a subclass of int (including int itself) T = TypeVar("T", bound=int) + def increment(val: T) -> T: - return val + 1 # Error: + 1 returns int, but we must return T + return val + 1 # Error: + 1 returns int, but we must return T ``` - **Pro Tip**: Use a `Protocol` as a bound to restrict a `TypeVar` to objects with specific methods. ```python from typing import Protocol, TypeVar + class SupportsRead(Protocol): def read(self) -> str: ... + T = TypeVar("T", bound=SupportsRead) ``` diff --git a/.agents/skills/type-checking/references/naming.md b/.agents/skills/type-checking/references/naming.md index bc04b4eb..38ce4f6d 100644 --- a/.agents/skills/type-checking/references/naming.md +++ b/.agents/skills/type-checking/references/naming.md @@ -15,6 +15,7 @@ T = TypeVar("T") KT = TypeVar("KT") VT = TypeVar("VT") + def get_keys(data: Mapping[KT, VT]) -> list[KT]: return list(data.keys()) ``` @@ -28,6 +29,7 @@ Follow the standard Python conventions for protocols: ```python from typing import Protocol + class SupportsMerge(Protocol): def merge(self, other: object) -> object: ... ``` diff --git a/.agents/skills/type-checking/references/protocol-patterns.md b/.agents/skills/type-checking/references/protocol-patterns.md index 295fd00b..d27b3839 100644 --- a/.agents/skills/type-checking/references/protocol-patterns.md +++ b/.agents/skills/type-checking/references/protocol-patterns.md @@ -9,11 +9,13 @@ Instead of checking `isinstance(obj, pathlib.Path)`, check if it "works like a p ```python from typing import Protocol, runtime_checkable + @runtime_checkable class PathLike(Protocol): def exists(self) -> bool: ... def read_text(self) -> str: ... + def process(path: PathLike) -> str: if path.exists(): return path.read_text() @@ -27,10 +29,12 @@ When a library returns `Any`, use a `Protocol` to "tame" it without using `cast` ```python from typing import Protocol, Any + class ConfigContainer(Protocol): def get(self, key: str) -> Any: ... def keys(self) -> list[str]: ... + def load_config(raw: Any) -> ConfigContainer: # No cast needed if the argument is structural return raw @@ -43,9 +47,10 @@ For recursive structures (like nested dicts or file trees), use a `Protocol` tha ```python from typing import Protocol, Union, Optional + class NestedDict(Protocol): - def __getitem__(self, key: str) -> Union[str, 'NestedDict']: ... - def get(self, key: str) -> Optional[Union[str, 'NestedDict']]: ... + def __getitem__(self, key: str) -> Union[str, "NestedDict"]: ... + def get(self, key: str) -> Optional[Union[str, "NestedDict"]]: ... ``` ## Best Practices diff --git a/.agents/skills/type-checking/references/refactoring-patterns.md b/.agents/skills/type-checking/references/refactoring-patterns.md index dd723cb0..5c9de230 100644 --- a/.agents/skills/type-checking/references/refactoring-patterns.md +++ b/.agents/skills/type-checking/references/refactoring-patterns.md @@ -13,10 +13,12 @@ Simple containers or return types that provide immediate clarity over `Any`. ```python T = TypeVar("T") + class Result(Generic[T]): def __init__(self, value: T) -> None: self.value = value + # Result[Project] is 10x clearer than Mapping[str, object]. ``` @@ -27,6 +29,7 @@ Bound TypeVars that restrict a Generic to a specific hierarchy or protocol. ```python T = TypeVar("T", bound=Mapping[str, object]) + def merge_configs(base: T, update: T) -> T: # Guaranteed to return the same specific type (e.g. Table). ... @@ -60,6 +63,7 @@ Stop refactoring and use a simple `isinstance` or `# type: ignore[code]` if: ```python from typing import Mapping + def deep_navigate(data: Mapping[str, object], path: list[str]) -> Optional[str]: # Navigate using a simple loop and isinstance narrowing. # Type-safe, human-readable, and 100x easier to maintain. diff --git a/.agents/skills/warnings-control/SKILL.md b/.agents/skills/warnings-control/SKILL.md index 6706e5a6..b0779a85 100644 --- a/.agents/skills/warnings-control/SKILL.md +++ b/.agents/skills/warnings-control/SKILL.md @@ -22,10 +22,13 @@ When working on a library or a CLI tool, you should almost always define a custo # src/my_project/exceptions.py class ProjectWarning(UserWarning): """Base category for warnings related to this project.""" + pass + class ConfigWarning(ProjectWarning): """Category for configuration-related warnings.""" + pass ``` @@ -54,6 +57,7 @@ When implementing a `--strict` mode, use `warnings.filterwarnings` to target *on import warnings from my_project.exceptions import ProjectWarning + def enable_strict_mode(): """Convert only this project's warnings into exceptions.""" # This ensures third-party dependency warnings are left alone, @@ -88,6 +92,7 @@ When testing code that raises warnings, wrap the operation in `warnings.catch_wa ```python import warnings + def test_deprecated_feature(): with warnings.catch_warnings(record=True) as w: # Guarantee all warnings are captured instead of being filtered diff --git a/.agents/tui_design.md b/.agents/tui_design.md index 2f28c585..cebe789b 100644 --- a/.agents/tui_design.md +++ b/.agents/tui_design.md @@ -39,16 +39,18 @@ Right now, discovering and extracting the local `pyproject.toml` is slightly cou inspect_parser = subparsers.add_parser( "inspect", parents=[common_parser], - help="Open a Terminal UI to explore and interrogate your local ruff configuration." + help="Open a Terminal UI to explore and interrogate your local ruff configuration.", ) ``` - In `main()`, route the `inspect` command to a lazy-loaded wrapper: ```python if exec_args.command == "inspect": from ruff_sync.dependencies import require_dependency + require_dependency("textual", extra_name="tui") from ruff_sync.tui.app import RuffSyncApp + return RuffSyncApp(exec_args).run() ``` diff --git a/.agents/workflows/update-screenshots.md b/.agents/workflows/update-screenshots.md index 59d5db47..05658660 100644 --- a/.agents/workflows/update-screenshots.md +++ b/.agents/workflows/update-screenshots.md @@ -41,7 +41,7 @@ To add a new view to the automated screenshot rotation: ```python # Navigate to the new view await pilot.press("control+f") # Example: Open a specific dialog - await pilot.pause(0.2) # Give the UI time to animate + await pilot.pause(0.2) # Give the UI time to animate ``` 3. **Capture the Screen**: ```python diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4038dab0..0f6c79fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: no-commit-to-branch args: [--branch, develop, --branch, main] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.16.2" + rev: "v0.16.3" hooks: - id: ruff-check args: ["--fix"] diff --git a/pyproject.toml b/pyproject.toml index e0ca02a4..ab1b373e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -214,6 +214,7 @@ ignore = [ "S106", # hard-code passwords in tests should be fake "PLR2004", # magic values in test comparison is less of a concern "PLR0913", # too many arguments in test functions + "PLR0917", # too many positional arguments in test functions "PLC0415", # lazy imports needed for some tests and fixture setup "FBT001", # boolean typed positional arguments common in parametrized tests "FBT003", # Boolean positional value in test code is common and less of a concern diff --git a/tests/ruff.toml b/tests/ruff.toml index 11555ac4..e607b578 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -15,6 +15,8 @@ lint.extend-ignore = [ # https://beta.ruff.rs/docs/rules/#flake8-bugbear-b "B011", # assert-false - common pattern in pytest # https://beta.ruff.rs/docs/rules/#flake8-datetimez-dtz + "PLR0913", # too many arguments in test functions + "PLR0917", # too many positional arguments in test functions "PLR2004", # magic value comparison is common test pattern "RUF015", # element index slice is common test pattern # we don't need to enforce these security rules for tests diff --git a/uv.lock b/uv.lock index c3f3a83b..cb2e053c 100644 --- a/uv.lock +++ b/uv.lock @@ -1731,27 +1731,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, - { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, - { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, - { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, - { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, - { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, - { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, - { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, - { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, ] [[package]]