diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f9297ae..71eafc3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: # Ruff: fast Python linter and formatter - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.16.4" + rev: "v0.16.5" hooks: - id: ruff-check # lints Python code and auto-fixes where possible args: ["--fix"] diff --git a/AGENTS.md b/AGENTS.md index 5dfeb7eb..83394f1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ Specific workflows, libraries, and tools are documented in `.agents/skills/`. Be - **Python** ≥ 3.11 (target version `py311`) - **Package Manager**: [uv](https://docs.astral.sh/uv/) — Use `uv run ` for all executions to ensure the correct environment. - **Note on PATH**: On macOS, `uv` is often installed in `~/.local/bin`. If `uv` is not found, add this to your `PATH`: `export PATH="$PATH:$HOME/.local/bin"`. -- **Linter / Formatter**: [Ruff](https://docs.astral.sh/ruff/) (`>=0.15.0`) +- **Linter / Formatter**: [Ruff](https://docs.astral.sh/ruff/) (`>=0.16.5`) - **Type Checker**: [mypy](https://mypy-lang.org/) (strict mode) - **Test Framework**: [pytest](https://docs.pytest.org/) with `pytest-asyncio`, `respx`, `pyfakefs` (See [Testing Standards](.agents/TESTING.md)) - **Coverage**: `coverage` + Codecov diff --git a/configs/data-science-engineering/ruff.toml b/configs/data-science-engineering/ruff.toml index aec45616..ebb10f79 100644 --- a/configs/data-science-engineering/ruff.toml +++ b/configs/data-science-engineering/ruff.toml @@ -1,25 +1,209 @@ -# configs/data-science-engineering/ruff.toml +# ruff-sync Data Science & Engineering Configuration +# https://github.com/Kilo59/ruff-sync +# +# This is a Ruff configuration tailored for Data Science, Machine Learning, +# and Data Engineering workflows. +# It enables rules for Jupyter Notebooks, NumPy, Pandas, Airflow, and performance. +# +# Usage (direct Ruff config, assuming this file is vendored into your repo): +# extend = "configs/data-science-engineering/ruff.toml" +# +# Usage (with ruff-sync in pyproject.toml): +# [tool.ruff-sync] +# path = "configs/data-science-engineering/ruff.toml" -# Enable Jupyter notebook linting +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.10. Consumers should override this to match their project's Python version. +target-version = "py310" + +# Enable Jupyter notebook linting and formatting extend-include = ["*.ipynb"] extend-exclude = [".ipynb_checkpoints"] [lint] -# Enable rules tailored for data science and engineering. -extend-select = [ +# Enable rules tailored for data science, machine learning, and data engineering. +select = [ + # https://docs.astral.sh/ruff/rules/#pyflakes-f + "F", # Pyflakes: Essential checks for Python bugs + # https://docs.astral.sh/ruff/rules/#error-e + "E", # pycodestyle errors: PEP8 styling + # https://docs.astral.sh/ruff/rules/#warning-w + "W", # pycodestyle warnings: PEP8 styling + # https://docs.astral.sh/ruff/rules/#mccabe-c90 + "C90", # mccabe: Code complexity (cyclomatic complexity) # https://docs.astral.sh/ruff/rules/#isort-i "I", # isort: Import sorting - # https://docs.astral.sh/ruff/rules/#numpy-specific-rules-npy - "NPY", # NumPy-specific rules: NumPy conventions + # https://docs.astral.sh/ruff/rules/#pep8-naming-n + "N", # pep8-naming: Naming conventions + # https://docs.astral.sh/ruff/rules/#pydocstyle-d + "D", # pydocstyle: Docstring conventions (NumPy style) + # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "UP", # pyupgrade: Upgrade syntax for newer Python versions + # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + "ANN", # flake8-annotations: Type annotation checks + # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "B", # flake8-bugbear: Finding likely bugs and design problems + # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "A", # flake8-builtins: Check for python builtins being used as variables + # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "C4", # flake8-comprehensions: Better list/set/dict comprehensions + # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "DTZ", # flake8-datetimez: Usage of unsafe naive datetime class + # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 + "T10", # flake8-debugger: Check for pdb/ipdb imports and set_traces + # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "G", # flake8-logging-format: Validate logging format strings + # https://docs.astral.sh/ruff/rules/#flake8-pie-pie + "PIE", # flake8-pie: Misc. lints + # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + "T20", # flake8-print: Check for Print statements + # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "PT", # flake8-pytest-style: Pytest style checks + # https://docs.astral.sh/ruff/rules/#flake8-quotes-q + "Q", # flake8-quotes: Lint for quotes + # https://docs.astral.sh/ruff/rules/#flake8-return-ret + "RET", # flake8-return: Check return values + # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "SIM", # flake8-simplify: Code simplification + # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg + "ARG", # flake8-unused-arguments: Unused argument checks + # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + "PTH", # flake8-use-pathlib: Use pathlib instead of os.path # https://docs.astral.sh/ruff/rules/#pandas-vet-pd "PD", # pandas-vet: Pandas code checks + # https://docs.astral.sh/ruff/rules/#numpy-specific-rules-npy + "NPY", # NumPy-specific rules: NumPy conventions & NumPy 2.0 migration + # https://docs.astral.sh/ruff/rules/#airflow-air + "AIR", # Airflow: Airflow best practices (Data Engineering pipelines) + # https://docs.astral.sh/ruff/rules/#perflint-perf + "PERF", # Perflint: Performance anti-patterns in loops + # https://docs.astral.sh/ruff/rules/#refurb-furb + "FURB", # refurb: Modernize Python code + # https://docs.astral.sh/ruff/rules/#flake8-logging-log + "LOG", # flake8-logging: Better logging practices + # https://docs.astral.sh/ruff/rules/#pylint-pl + "PL", # Pylint: Pylint rules + # https://docs.astral.sh/ruff/rules/#flynt-fly + "FLY", # flynt: Convert string formatting to f-strings + # https://docs.astral.sh/ruff/rules/#pydoclint-doc + # "DOC", # pydoclint: Validate docstrings against function signatures (Note: requires `preview = true`) + # https://docs.astral.sh/ruff/rules/pytest-fixture-autouse/ + # "pedantic", # Category selector: Pedantic checks (Note: requires `preview = true`) + # "pytest-fixture-autouse", # Codeless rule: Avoid autouse=True in pytest fixtures (Note: requires `preview = true`) + # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + "RUF", # Ruff-specific rules: Rules unique to Ruff ] -# Ignore rules that conflict with `ruff format` (formatter-managed concerns). -# Keep this in sync with other curated configs (e.g. fastapi/ruff.toml). + +# Preview mode settings: +# preview = true # Enable preview rules and fixes across Ruff +# explicit-preview-rules = true # When preview is enabled, require full rule codes instead of prefixes + +# Ignore rules that conflict with the Ruff formatter. +# See: https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules ignore = [ - "E111", # indentation is handled by the formatter - "E114", # indentation with comments is handled by the formatter - "E117", # over-indented code is handled by the formatter - "E501", # line length is handled by the formatter - "W191", # tabs vs spaces is handled by the formatter + "W191", # tab-indentation + "E111", # indentation-with-invalid-multiple + "E114", # indentation-with-invalid-multiple-comment + "E117", # over-indented + "D206", # docstring-tab-indentation + "D300", # triple-single-quotes + "Q000", # bad-quotes-inline-string + "Q001", # bad-quotes-multiline-string + "Q002", # bad-quotes-docstring + "Q003", # avoidable-escaped-quote + "Q004", # unnecessary-escaped-quote + "COM812", # missing-trailing-comma + "COM819", # prohibited-trailing-comma + "ISC001", # single-line-implicit-string-concatenation + "ISC002", # multi-line-implicit-string-concatenation ] + +# Allow autofix for all enabled rules. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[lint.per-file-ignores] +# Notebooks often have top-level statements, plots, and exploratory code. +"*.ipynb" = [ + "E402", # Module level import not at top of file + "T201", # print found + "D100", # Missing docstring in public module + "D103", # Missing docstring in public function +] + +[lint.pydocstyle] +# Use NumPy-style docstrings (standard in Data Science / Scientific Python). +# Default: "pep257" +convention = "numpy" + +[lint.mccabe] +# Flag errors (C901) whenever the complexity level exceeds 10. +# Default: 10 +max-complexity = 10 + +[lint.flake8-quotes] +# Use double quotes for inline strings (same as Black). +# Default: "double" +inline-quotes = "double" +# Use double quotes for multiline strings. +# Default: "double" +multiline-quotes = "double" +# Use double quotes for docstrings. +# Default: "double" +docstring-quotes = "double" +# Avoid escaping quotes if the other quote type would save an escape. +# Default: true +avoid-escape = true + +[lint.flake8-pytest-style] +# Whether to require parentheses for pytest fixtures. +# Default: true +fixture-parentheses = true +# The type of pytest.mark.parametrize names to use: "tuple", "list", or "csv" +# Default: "tuple" +parametrize-names-type = "tuple" + +[lint.pylint] +# The maximum number of arguments allowed for a function. +# Default: 5 +max-args = 5 +# The maximum number of branches allowed for a function. +# Default: 12 +max-branches = 12 + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = true + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" + +# Controls the quote style for nested strings inside interpolated string expressions (Python 3.12+). +# Can be "alternating" (default) or "preferred". +nested-string-quote-style = "alternating" diff --git a/configs/fastapi/ruff.toml b/configs/fastapi/ruff.toml index 903e0a08..825a71d5 100644 --- a/configs/fastapi/ruff.toml +++ b/configs/fastapi/ruff.toml @@ -1,8 +1,8 @@ # ruff-sync FastAPI Configuration # https://github.com/Kilo59/ruff-sync # -# This is a Ruff configuration tailored for FastAPI / Web App development. -# It enables rules directly relevant to web development and async Python. +# This is a Ruff configuration tailored for FastAPI and modern async web applications. +# It enables rules directly relevant to web development, async Python, and Pydantic models. # # Usage (direct Ruff config, assuming this file is vendored into your repo): # extend = "configs/fastapi/ruff.toml" @@ -43,48 +43,92 @@ select = [ "ASYNC", # flake8-async: Asynchronous code checks # https://docs.astral.sh/ruff/rules/#flake8-bandit-s "S", # flake8-bandit: Security testing + # https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble + "BLE", # flake8-blind-except: Checks for blind except: statements # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b "B", # flake8-bugbear: Finding likely bugs and design problems + # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "A", # flake8-builtins: Check for python builtins being used as variables + # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "C4", # flake8-comprehensions: Write better list/set/dict comprehensions + # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "DTZ", # flake8-datetimez: Usage of unsafe naive datetime class + # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 + "T10", # flake8-debugger: Check for pdb/ipdb imports and set_traces + # https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa + "FA", # flake8-future-annotations: Verify python 3.7+ from __future__ import annotations + # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "G", # flake8-logging-format: Validate logging format strings + # https://docs.astral.sh/ruff/rules/#flake8-logging-log + "LOG", # flake8-logging: Better logging practices # https://docs.astral.sh/ruff/rules/#flake8-pie-pie "PIE", # flake8-pie: Misc. lints # https://docs.astral.sh/ruff/rules/#flake8-print-t20 "T20", # flake8-print: Check for Print statements + # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "PT", # flake8-pytest-style: Pytest style checks + # https://docs.astral.sh/ruff/rules/#flake8-quotes-q + "Q", # flake8-quotes: Lint for quotes + # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + "RSE", # flake8-raise: Find and correct raise statements # https://docs.astral.sh/ruff/rules/#flake8-return-ret "RET", # flake8-return: Check return values # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim "SIM", # flake8-simplify: Code simplification - # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g - "G", # flake8-logging-format: Validate logging format strings - # https://docs.astral.sh/ruff/rules/#flake8-quotes-q - "Q", # flake8-quotes: Lint for quotes - # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - "PT", # flake8-pytest-style: Pytest style checks + # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid + "TID", # flake8-tidy-imports: Tidy imports + # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc + "TC", # flake8-type-checking: Move imports into type-checking blocks + # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg + "ARG", # flake8-unused-arguments: Unused argument checks + # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + "PTH", # flake8-use-pathlib: Use pathlib instead of os.path + # https://docs.astral.sh/ruff/rules/#flynt-fly + "FLY", # flynt: Convert string formatting to f-strings + # https://docs.astral.sh/ruff/rules/#perflint-perf + "PERF", # Perflint: Performance anti-patterns + # https://docs.astral.sh/ruff/rules/#refurb-furb + "FURB", # refurb: Modernize Python code # https://docs.astral.sh/ruff/rules/#pylint-pl "PL", # Pylint: Pylint rules + # https://docs.astral.sh/ruff/rules/#fastapi-fast + "FAST", # FastAPI: FastAPI-specific rules + # https://docs.astral.sh/ruff/rules/#pydoclint-doc + # "DOC", # pydoclint: Validate docstrings against function signatures (Note: requires `preview = true`) + # https://docs.astral.sh/ruff/rules/pytest-fixture-autouse/ + # "pedantic", # Category selector: Pedantic checks (Note: requires `preview = true`) + # "pytest-fixture-autouse", # Codeless rule: Avoid autouse=True in pytest fixtures (Note: requires `preview = true`) + # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + "RUF", # Ruff-specific rules: Rules unique to Ruff ] +# Preview mode settings: +# preview = true # Enable preview rules and fixes across Ruff +# explicit-preview-rules = true # When preview is enabled, require full rule codes instead of prefixes + # Ignore rules that conflict with the Ruff formatter. # See: https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules ignore = [ - "W191", # tab-indentation - "E111", # indentation-with-invalid-multiple - "E114", # indentation-with-invalid-multiple-comment - "E117", # over-indented - "D206", # docstring-tab-indentation - "D300", # triple-single-quotes - "Q000", # bad-quotes-inline-string - "Q001", # bad-quotes-multiline-string - "Q002", # bad-quotes-docstring - "Q003", # avoidable-escaped-quote - "Q004", # unnecessary-escaped-quote - "COM812", # missing-trailing-comma - "COM819", # prohibited-trailing-comma - "ISC001", # single-line-implicit-string-concatenation - "ISC002", # multi-line-implicit-string-concatenation + "W191", # tab-indentation + "E111", # indentation-with-invalid-multiple + "E114", # indentation-with-invalid-multiple-comment + "E117", # over-indented + "D206", # docstring-tab-indentation + "D300", # triple-single-quotes + "Q000", # bad-quotes-inline-string + "Q001", # bad-quotes-multiline-string + "Q002", # bad-quotes-docstring + "Q003", # avoidable-escaped-quote + "Q004", # unnecessary-escaped-quote + "COM812", # missing-trailing-comma + "COM819", # prohibited-trailing-comma + "ISC001", # single-line-implicit-string-concatenation + "ISC002", # multi-line-implicit-string-concatenation ] # Allow autofix for all enabled rules. fixable = ["ALL"] +unfixable = [] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" @@ -142,9 +186,30 @@ classmethod-decorators = [ [format] # Like Black, use double quotes for strings. quote-style = "double" + # Like Black, indent with spaces, rather than tabs. indent-style = "space" + # Like Black, respect magic trailing commas. skip-magic-trailing-comma = false + # Like Black, automatically detect the appropriate line ending. line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = true + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" + +# Controls the quote style for nested strings inside interpolated string expressions (Python 3.12+). +# Can be "alternating" (default) or "preferred". +nested-string-quote-style = "alternating" diff --git a/configs/kitchen-sink/ruff.toml b/configs/kitchen-sink/ruff.toml index 2acac1eb..36ed02f7 100644 --- a/configs/kitchen-sink/ruff.toml +++ b/configs/kitchen-sink/ruff.toml @@ -1,7 +1,7 @@ # ruff-sync Kitchen Sink Configuration # https://github.com/Kilo59/ruff-sync # -# This file enables ALL possible Ruff rules as of Ruff v0.15.5 +# This file enables ALL possible Ruff rules as of Ruff v0.16.5 # It explicitly lists all rule categories and provides links to their documentation. # This serves as a comprehensive reference for what is possible with Ruff. @@ -51,7 +51,7 @@ select = [ # https://docs.astral.sh/ruff/rules/#flake8-commas-com "COM", # flake8-commas: Trailing commas checks # https://docs.astral.sh/ruff/rules/#flake8-copyright-cpy - # "CPY", # flake8-copyright: Copyright notice checks (Note: requires `preview = true`) + "CPY", # flake8-copyright: Copyright notice checks # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 "C4", # flake8-comprehensions: Write better list/set/dict comprehensions # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz @@ -99,6 +99,8 @@ select = [ # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc "TC", # flake8-type-checking: Move imports into type-checking blocks # https://docs.astral.sh/ruff/rules/#flake8-gettext-int + "INT", # flake8-gettext: Internationalization / gettext checks + # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg "ARG", # flake8-unused-arguments: Unused argument checks # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth "PTH", # flake8-use-pathlib: Use pathlib instead of os.path @@ -128,12 +130,21 @@ select = [ "FURB", # refurb: Modernize Python code # https://docs.astral.sh/ruff/rules/#flake8-logging-log "LOG", # flake8-logging: Better logging practices + # https://docs.astral.sh/ruff/rules/#fastapi-fast + "FAST", # FastAPI: FastAPI-specific rules # https://docs.astral.sh/ruff/rules/#pydoclint-doc - # "DOC", # pydoclint: Validate docstrings against function signatures (Note: requires `preview = true`) + # "DOC", # pydoclint: Validate docstrings against function signatures (Note: requires `preview = true`) + # https://docs.astral.sh/ruff/rules/pytest-fixture-autouse/ + # "pedantic", # Category selector: Pedantic checks (Note: requires `preview = true`) + # "pytest-fixture-autouse", # Codeless rule: Avoid autouse=True in pytest fixtures (Note: requires `preview = true`) # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf - "RUF" # Ruff-specific rules: Rules unique to Ruff + "RUF", # Ruff-specific rules: Rules unique to Ruff ] +# Preview mode settings: +# preview = true # Enable preview rules and fixes across Ruff +# explicit-preview-rules = true # When preview is enabled, require full rule codes instead of prefixes + # We don't ignore any rules in this config, as it aims to be exhaustive. # WARNING: Some of the rules enabled above may be mutually exclusive or # conflict with each other (e.g., D212 and D213). You will likely need to @@ -228,6 +239,10 @@ docstring-code-format = true # enabled. docstring-code-line-length = "dynamic" +# Controls the quote style for nested strings inside interpolated string expressions (Python 3.12+). +# Can be "alternating" (default) or "preferred". +nested-string-quote-style = "alternating" + [lint.flake8-pytest-style] # Whether to require parentheses for pytest fixtures. # Default: true @@ -259,4 +274,17 @@ ignore-names = [] classmethod-decorators = [ "pydantic.validator", "pydantic.root_validator", + "pydantic.field_validator", + "pydantic.model_validator", ] + +[lint.flake8-copyright] +# Copyright notice regex pattern. +# notice-rgx = "(?i)Copyright \\(C\\) \\d{4}" + +[lint.flake8-gettext] +# Additional function names to consider as gettext calls. +# extend-function-names = ["_"] + +# [lint.pydoclint] +# ignore-one-line-docstrings = true # (Note: requires `preview = true`) diff --git a/docs/pre-defined-configs.md b/docs/pre-defined-configs.md index 3ee9d4c9..37d860b4 100644 --- a/docs/pre-defined-configs.md +++ b/docs/pre-defined-configs.md @@ -11,7 +11,7 @@ These configurations are maintained in the [`configs/`](https://github.com/Kilo5 An exhaustive configuration that explicitly enables and documents almost all available Ruff rules. This is ideal for teams that want a strict, "no-stone-unturned" approach to linting and formatting. ### Key Features -- **Strict Linting**: Enables almost all Ruff rules (over 700 rules). +- **Strict Linting**: Enables almost all Ruff rules (over 900 rules). - **Explicit Documentation**: Each rule or category is documented with comments explaining why it's enabled. - **Safety First**: Includes security-related rules from `flake8-bandit`. diff --git a/pyproject.toml b/pyproject.toml index 86cdc76c..be81b4d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ruff-sync" -version = "0.1.9.dev0" +version = "0.1.9.dev1" description = "Synchronize Ruff linter configuration across projects" keywords = ["ruff", "linter", "config", "synchronize", "python", "linting", "automation", "tomlkit", "pre-commit"] authors = [ @@ -58,7 +58,7 @@ dev = [ "pytest-textual-snapshot>=1.0.0", "respx>=0.23.1", "ruamel-yaml>=0.18.6", - "ruff>=0.16.0", + "ruff>=0.16.5", "textual>=8.2.2", "wily>=1.25.0", ] @@ -115,6 +115,7 @@ exclude = [ "lint.per-file-ignores", "lint.ignore", "lint.flake8-tidy-imports.banned-api", + "lint.flake8-import-conventions.banned-from", "lint.isort.required-imports", ] @@ -221,7 +222,6 @@ ignore = [ "PLW1510", # subprocess check not needed in tests "PT013", # pytest import style, prevents importing param and or types in from style "TC002", # Move third-party import into type-checking block - "TID251", # allow cast in tests ] [tool.ruff.lint.pydocstyle] @@ -243,7 +243,7 @@ docstring-code-line-length = "dynamic" [tool.ruff.lint.flake8-import-conventions] # Declare the banned `from` imports. -banned-from = ["pathlib", "datetime"] +banned-from = ["pathlib", "datetime", "unittest.mock"] [tool.ruff.lint.flake8-import-conventions.aliases] # Declare the default aliases. diff --git a/src/ruff_sync/system.py b/src/ruff_sync/system.py index 0b55fb1f..8cc0eca3 100644 --- a/src/ruff_sync/system.py +++ b/src/ruff_sync/system.py @@ -16,10 +16,10 @@ async def get_ruff_rule_markdown(rule_code: str) -> str | None: - """Execute `ruff rule ` and return the Markdown documentation. + """Execute `ruff rule ` and return the Markdown documentation. Args: - rule_code: The Ruff rule code (e.g., 'RUF012'). + rule_code: The Ruff rule code or rule name (e.g., 'RUF012' or 'unused-imports'). Returns: The Markdown documentation for the rule, or None if the execution fails @@ -115,18 +115,20 @@ def compute_effective_rules( enriched: list[RuffRule] = [] for rule in all_rules: - code = rule["code"] + code = rule.get("code") + name = rule.get("name", "") + category = rule.get("category", "") # Find longest matching select prefix best_select_len = -1 for s in select: - if code.startswith(s): + if (code and code.startswith(s)) or s in (name, category): best_select_len = max(best_select_len, len(s)) # Find longest matching ignore prefix best_ignore_len = -1 for i in ignore: - if code.startswith(i): + if (code and code.startswith(i)) or i in (name, category): best_ignore_len = max(best_ignore_len, len(i)) status = "Disabled" diff --git a/src/ruff_sync/tui/app.py b/src/ruff_sync/tui/app.py index ae4f1ad2..7413f4f8 100644 --- a/src/ruff_sync/tui/app.py +++ b/src/ruff_sync/tui/app.py @@ -218,10 +218,17 @@ def _inspect_rule(self, rule_code: str) -> None: """Centralized helper for rule inspection with metadata enrichment. Args: - rule_code: The Ruff rule code to inspect. + rule_code: A Ruff rule code or rule name to inspect. """ # Fetch metadata for enrichment - rule_data = next((r for r in self.effective_rules if r["code"] == rule_code), None) + rule_data = next( + ( + r + for r in self.effective_rules + if r.get("code") == rule_code or r.get("name") == rule_code + ), + None, + ) name = rule_data.get("name") if rule_data else None status = str(rule_data.get("status", "Disabled")) if rule_data else "Disabled" explanation = rule_data.get("explanation") if rule_data else None diff --git a/src/ruff_sync/tui/screens.py b/src/ruff_sync/tui/screens.py index ee8431db..f35a70ac 100644 --- a/src/ruff_sync/tui/screens.py +++ b/src/ruff_sync/tui/screens.py @@ -86,17 +86,16 @@ def handle_input_changed(self, event: Input.Changed) -> None: matches = [] for rule in self.all_rules: - code = rule["code"].lower() + code = (rule.get("code") or "").lower() name = rule["name"].lower() - if search_query in code or search_query in name: + if (code and search_query in code) or search_query in name: matches.append(rule) if len(matches) >= MAX_SEARCH_RESULTS: # Limit results break for match in matches: - results_list.add_option( - Option(f"[b]{match['code']}[/b] - {match['name']}", id=match["code"]) - ) + ident = match.get("code") or match["name"] + results_list.add_option(Option(f"[b]{ident}[/b] - {match['name']}", id=ident)) @on(Input.Submitted) def handle_input_submitted(self) -> None: diff --git a/src/ruff_sync/tui/types_.py b/src/ruff_sync/tui/types_.py index e4a3fbde..f6b3d434 100644 --- a/src/ruff_sync/tui/types_.py +++ b/src/ruff_sync/tui/types_.py @@ -208,8 +208,9 @@ class RuleNode: def __init__(self, rule: RuffRule) -> None: """Initialize a Rule Node.""" self.rule = rule - self._key = rule["code"] - self._path = f"__rule__:{rule['code']}" + rule_ident = rule.get("code") or rule["name"] + self._key = rule_ident + self._path = f"__rule__:{rule_ident}" @property def key(self) -> str: @@ -227,13 +228,47 @@ def children(self) -> list[ConfigNode]: def doc_target(self) -> tuple[str, Literal["rule", "config", "none"]]: """Target the rule exactly for documentation.""" - return (self.rule["code"], "rule") + return (self.rule.get("code") or self.rule["name"], "rule") -def _is_linter_active(linter: RuffLinter, effective_rules: list[RuffRule]) -> bool: +def rule_matches_linter(rule: RuffRule, linter: RuffLinter) -> bool: + """Check if a rule belongs to a given linter or category group.""" + linter_name = linter["name"] prefix = linter.get("prefix") - if prefix and any( - r["code"].startswith(prefix) and r["status"] != "Disabled" for r in effective_rules + rule_code = rule.get("code") + rule_linter = rule.get("linter") + rule_category = rule.get("category") + rule_name = rule.get("name") + + # 1. Direct linter name match if rule has linter specified + if rule_linter and rule_linter.lower() == linter_name.lower(): + return True + + # 2. Prefix match if both rule code and linter prefix exist + if prefix and rule_code and rule_code.startswith(prefix): + return True + + # 3. Fallback for codeless rules or rules without linter field: + # Match category or rule name against linter name or prefix + if not rule_linter or not rule_code: + if rule_category and ( + rule_category.lower() == linter_name.lower() + or (prefix and rule_category.lower() == prefix.lower()) + ): + return True + if rule_name and ( + rule_name.lower() == linter_name.lower() + or rule_name.lower().startswith(linter_name.lower()) + or (prefix and rule_name.lower().startswith(prefix.lower())) + ): + return True + + return False + + +def _is_linter_active(linter: RuffLinter, effective_rules: list[RuffRule]) -> bool: + if any( + rule_matches_linter(r, linter) and r.get("status") != "Disabled" for r in effective_rules ): return True diff --git a/src/ruff_sync/tui/widgets.py b/src/ruff_sync/tui/widgets.py index 61a4b6da..c427ecbd 100644 --- a/src/ruff_sync/tui/widgets.py +++ b/src/ruff_sync/tui/widgets.py @@ -17,6 +17,7 @@ ListNode, RulesCollectionNode, ScalarNode, + rule_matches_linter, ) if TYPE_CHECKING: @@ -138,8 +139,7 @@ def _(self, node: RulesCollectionNode) -> None: @render_node.register def _(self, node: LinterNode) -> None: self._reset_columns("Code", "Name", "Linter", "Fix") - linter_name = node.linter["name"] - filtered = [r for r in node.effective_rules if r["linter"] == linter_name] + filtered = [r for r in node.effective_rules if rule_matches_linter(r, node.linter)] self._render_rules(filtered) def _render_rules(self, rules: list[RuffRule]) -> None: @@ -172,9 +172,14 @@ def _render_rules(self, rules: list[RuffRule]) -> None: elif status == "Disabled": status_clr = "dim" - code_markup = f"[{status_clr}]{rule['code']}[/]" if status_clr else rule["code"] - name_markup = f"[{status_clr}]{rule['name']}[/]" if status_clr else rule["name"] - linter_markup = f"[{status_clr}]{rule['linter']}[/]" if status_clr else rule["linter"] + code_val = rule.get("code") or "-" + name_val = rule["name"] + linter_val = rule.get("linter") or rule.get("category") or "-" + rule_key = rule.get("code") or rule["name"] + + code_markup = f"[{status_clr}]{code_val}[/]" if status_clr else code_val + name_markup = f"[{status_clr}]{name_val}[/]" if status_clr else name_val + linter_markup = f"[{status_clr}]{linter_val}[/]" if status_clr else linter_val # Fix column uses its own color keyed on fix_availability: # Always → accent (e.g. magenta) @@ -188,7 +193,7 @@ def _render_rules(self, rules: list[RuffRule]) -> None: else: fix_markup = fix - self.add_row(code_markup, name_markup, linter_markup, fix_markup, key=rule["code"]) + self.add_row(code_markup, name_markup, linter_markup, fix_markup, key=rule_key) class RuleInspector(Markdown): diff --git a/src/ruff_sync/types_.py b/src/ruff_sync/types_.py index c7988b07..c0dd9983 100644 --- a/src/ruff_sync/types_.py +++ b/src/ruff_sync/types_.py @@ -10,10 +10,11 @@ class RuffRule(TypedDict): """Represents a single Ruff rule as returned by `ruff rule --all --output-format json`.""" - code: str + code: str | None name: str - linter: str + linter: str | None summary: str + category: NotRequired[str | None] explanation: NotRequired[str] fix_availability: NotRequired[str] status: NotRequired[RuleSyncStatus | dict[str, Any]] diff --git a/src/ruff_sync/validation.py b/src/ruff_sync/validation.py index 9f8e8242..3c4e5ecb 100644 --- a/src/ruff_sync/validation.py +++ b/src/ruff_sync/validation.py @@ -131,7 +131,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("code") and r.get("deprecated") is True) except ( FileNotFoundError, subprocess.TimeoutExpired, diff --git a/tests/ruff.toml b/tests/ruff.toml index e607b578..4e0345b3 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -31,3 +31,6 @@ lint.extend-ignore = [ [lint.isort] known-first-party = ["ruff_sync", "tests"] + +[lint.flake8-tidy-imports.banned-api] +"unittest.mock".msg = "Do not use unittest.mock. Prefer DI, respx, pyfakefs, or monkeypatch." diff --git a/tests/test_predefined_configs.py b/tests/test_predefined_configs.py new file mode 100644 index 00000000..7ffcf654 --- /dev/null +++ b/tests/test_predefined_configs.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import pathlib + +import pytest +import tomlkit + +from ruff_sync.validation import validate_ruff_accepts_config + +CONFIGS_DIR = pathlib.Path("configs") +CONFIG_NAMES = ["kitchen-sink", "fastapi", "data-science-engineering"] + + +@pytest.mark.parametrize("config_name", CONFIG_NAMES) +def test_predefined_configs_exist_and_valid_toml(config_name: str) -> None: + """Ensure every predefined configuration exists and is valid TOML.""" + config_path = CONFIGS_DIR / config_name / "ruff.toml" + assert config_path.is_file(), f"Expected config file not found: {config_path}" + + content = config_path.read_text(encoding="utf-8") + doc = tomlkit.parse(content) + assert isinstance(doc, tomlkit.TOMLDocument) + + +@pytest.mark.parametrize("config_name", CONFIG_NAMES) +def test_predefined_configs_accepted_by_ruff_strict(config_name: str) -> None: + """Ensure Ruff validates every predefined configuration without errors or warnings.""" + config_path = CONFIGS_DIR / config_name / "ruff.toml" + content = config_path.read_text(encoding="utf-8") + doc = tomlkit.parse(content) + + is_valid = validate_ruff_accepts_config(doc, is_ruff_toml=True, strict=True) + assert is_valid is True, f"Ruff rejected configuration {config_path}" + + +@pytest.mark.parametrize("config_name", CONFIG_NAMES) +def test_predefined_configs_core_structure(config_name: str) -> None: + """Ensure predefined configurations follow standardized core settings.""" + config_path = CONFIGS_DIR / config_name / "ruff.toml" + doc = tomlkit.parse(config_path.read_text(encoding="utf-8")) + + raw = doc.unwrap() + assert raw.get("line-length") == 88 + assert raw.get("indent-width") == 4 + assert raw.get("target-version") == "py310" + + assert "lint" in raw + lint = raw["lint"] + assert isinstance(lint.get("select"), list) + assert len(lint["select"]) > 0 + + assert "format" in raw + fmt = raw["format"] + assert fmt.get("quote-style") == "double" + assert fmt.get("indent-style") == "space" + assert fmt.get("docstring-code-format") is True + assert fmt.get("docstring-code-line-length") == "dynamic" + assert fmt.get("nested-string-quote-style") == "alternating" + + +def test_fastapi_config_specifics() -> None: + """Ensure FastAPI configuration has web and Pydantic rules configured.""" + config_path = CONFIGS_DIR / "fastapi" / "ruff.toml" + doc = tomlkit.parse(config_path.read_text(encoding="utf-8")) + raw = doc.unwrap() + + select = raw["lint"]["select"] + assert "FAST" in select + assert "ASYNC" in select + assert "LOG" in select + assert "RUF" in select + + assert raw["lint"]["pydocstyle"]["convention"] == "google" + + decorators = raw["lint"]["pep8-naming"]["classmethod-decorators"] + assert "pydantic.field_validator" in decorators + assert "pydantic.model_validator" in decorators + + +def test_data_science_config_specifics() -> None: + """Ensure Data Science & Engineering config has NumPy, Pandas, and notebook settings.""" + config_path = CONFIGS_DIR / "data-science-engineering" / "ruff.toml" + doc = tomlkit.parse(config_path.read_text(encoding="utf-8")) + raw = doc.unwrap() + + assert "*.ipynb" in raw.get("extend-include", []) + + select = raw["lint"]["select"] + assert "NPY" in select + assert "PD" in select + assert "AIR" in select + assert "PERF" in select + assert "LOG" in select + assert "RUF" in select + + assert raw["lint"]["pydocstyle"]["convention"] == "numpy" + + per_file = raw["lint"]["per-file-ignores"] + assert "*.ipynb" in per_file + assert "E402" in per_file["*.ipynb"] + assert "T201" in per_file["*.ipynb"] diff --git a/tests/test_rule_logic.py b/tests/test_rule_logic.py index 9fd4412e..4e92169d 100644 --- a/tests/test_rule_logic.py +++ b/tests/test_rule_logic.py @@ -100,3 +100,56 @@ def test_compute_effective_rules_extend(): # I001: Selected via extend-select i001 = next(r for r in enriched if r["code"] == "I001") assert i001["status"] == "Enabled" + + +def test_compute_effective_rules_codeless_and_category_rules(): + """Test handling of rules without rule codes and rules selected by name or category.""" + all_rules = [ + { + "code": None, + "name": "pytest-fixture-autouse", + "category": "pedantic", + "linter": None, + "summary": "Avoid using autouse=True", + }, + { + "code": "F401", + "name": "unused-import", + "category": None, + "linter": "Pyflakes", + "summary": "Unused import", + }, + ] + + # Select by rule name + toml_config_1 = { + "tool": { + "ruff": { + "lint": { + "select": ["pytest-fixture-autouse"], + } + } + } + } + enriched_1 = compute_effective_rules(cast("list[RuffRule]", all_rules), toml_config_1) + autouse_1 = next(r for r in enriched_1 if r["name"] == "pytest-fixture-autouse") + assert autouse_1["status"] == "Enabled" + + # Select by category + toml_config_2 = { + "tool": { + "ruff": { + "lint": { + "select": ["pedantic"], + } + } + } + } + enriched_2 = compute_effective_rules(cast("list[RuffRule]", all_rules), toml_config_2) + autouse_2 = next(r for r in enriched_2 if r["name"] == "pytest-fixture-autouse") + assert autouse_2["status"] == "Enabled" + + # Default (E, F) -> codeless rule is Disabled + enriched_default = compute_effective_rules(cast("list[RuffRule]", all_rules), {}) + autouse_def = next(r for r in enriched_default if r["name"] == "pytest-fixture-autouse") + assert autouse_def["status"] == "Disabled" diff --git a/tests/test_system.py b/tests/test_system.py index 67d13e3e..1995be79 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -3,52 +3,191 @@ from __future__ import annotations import asyncio -from unittest.mock import AsyncMock, patch +from typing import Any import pytest -from ruff_sync.system import get_ruff_rule_markdown +from ruff_sync.system import ( + compute_effective_rules, + get_all_ruff_rules, + get_ruff_config_markdown, + get_ruff_linters, + get_ruff_rule_markdown, +) +from ruff_sync.types_ import RuffRule + + +class FakeProcess: + """Fake asyncio subprocess for testing without unittest.mock.""" + + def __init__( + self, + stdout: bytes = b"", + stderr: bytes = b"", + returncode: int = 0, + ) -> None: + """Initialize FakeProcess with stdout, stderr, and returncode.""" + self._stdout = stdout + self._stderr = stderr + self.returncode = returncode + + async def communicate(self) -> tuple[bytes, bytes]: + """Simulate process communication by returning buffered stdout and stderr.""" + return self._stdout, self._stderr + + +class SubprocessSpy: + """Spy for asyncio.create_subprocess_exec calls.""" + + def __init__( + self, + fake_process: FakeProcess | None = None, + side_effect: Exception | None = None, + ) -> None: + """Initialize SubprocessSpy with an optional fake process or side effect exception.""" + self.called_args: list[tuple[Any, ...]] = [] + self.called_kwargs: list[dict[str, Any]] = [] + self.fake_process = fake_process or FakeProcess() + self.side_effect = side_effect + + async def __call__(self, *args: Any, **kwargs: Any) -> FakeProcess: + """Record invocation arguments and return the fake process or raise side effect.""" + self.called_args.append(args) + self.called_kwargs.append(kwargs) + if self.side_effect is not None: + raise self.side_effect + return self.fake_process @pytest.mark.asyncio -async def test_get_ruff_rule_markdown_success() -> None: - mock_process = AsyncMock() - mock_process.communicate.return_value = (b"RUF012 rule docs", b"") - mock_process.returncode = 0 +async def test_get_ruff_rule_markdown_success(monkeypatch: pytest.MonkeyPatch) -> None: + spy = SubprocessSpy(FakeProcess(stdout=b"RUF012 rule docs", returncode=0)) + monkeypatch.setattr(asyncio, "create_subprocess_exec", spy) - with patch("asyncio.create_subprocess_exec", return_value=mock_process) as mock_exec: - result = await get_ruff_rule_markdown("RUF012") - assert result == "RUF012 rule docs" - mock_exec.assert_called_once_with( - "ruff", - "rule", - "RUF012", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) + result = await get_ruff_rule_markdown("RUF012") + assert result == "RUF012 rule docs" + assert len(spy.called_args) == 1 + assert spy.called_args[0] == ("ruff", "rule", "RUF012") + assert spy.called_kwargs[0] == { + "stdout": asyncio.subprocess.PIPE, + "stderr": asyncio.subprocess.PIPE, + } @pytest.mark.asyncio -async def test_get_ruff_rule_markdown_error_code() -> None: - mock_process = AsyncMock() - mock_process.communicate.return_value = (b"", b"Rule not found") - mock_process.returncode = 1 +async def test_get_ruff_rule_markdown_by_name(monkeypatch: pytest.MonkeyPatch) -> None: + spy = SubprocessSpy(FakeProcess(stdout=b"unused-imports rule docs", returncode=0)) + monkeypatch.setattr(asyncio, "create_subprocess_exec", spy) - with patch("asyncio.create_subprocess_exec", return_value=mock_process): - result = await get_ruff_rule_markdown("NONEXISTENT") - assert result is None + result = await get_ruff_rule_markdown("unused-imports") + assert result == "unused-imports rule docs" + assert len(spy.called_args) == 1 + assert spy.called_args[0] == ("ruff", "rule", "unused-imports") + assert spy.called_kwargs[0] == { + "stdout": asyncio.subprocess.PIPE, + "stderr": asyncio.subprocess.PIPE, + } @pytest.mark.asyncio -async def test_get_ruff_rule_markdown_not_found() -> None: - with patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError): - result = await get_ruff_rule_markdown("RUF012") - assert result is None +async def test_get_ruff_rule_markdown_error_code(monkeypatch: pytest.MonkeyPatch) -> None: + spy = SubprocessSpy(FakeProcess(stderr=b"Rule not found", returncode=1)) + monkeypatch.setattr(asyncio, "create_subprocess_exec", spy) + + result = await get_ruff_rule_markdown("NONEXISTENT") + assert result is None + + +@pytest.mark.asyncio +async def test_get_ruff_rule_markdown_not_found(monkeypatch: pytest.MonkeyPatch) -> None: + spy = SubprocessSpy(side_effect=FileNotFoundError()) + monkeypatch.setattr(asyncio, "create_subprocess_exec", spy) + + result = await get_ruff_rule_markdown("RUF012") + assert result is None @pytest.mark.asyncio -async def test_get_ruff_rule_markdown_unexpected_exception() -> None: +async def test_get_ruff_rule_markdown_unexpected_exception(monkeypatch: pytest.MonkeyPatch) -> None: # Test that the generic Exception catch logs and returns None - with patch("asyncio.create_subprocess_exec", side_effect=RuntimeError("Unexpected error")): - result = await get_ruff_rule_markdown("RUF012") - assert result is None + spy = SubprocessSpy(side_effect=RuntimeError("Unexpected error")) + monkeypatch.setattr(asyncio, "create_subprocess_exec", spy) + + result = await get_ruff_rule_markdown("RUF012") + assert result is None + + +@pytest.mark.asyncio +async def test_get_ruff_config_markdown(monkeypatch: pytest.MonkeyPatch) -> None: + spy = SubprocessSpy(FakeProcess(stdout=b"lint.select docs", returncode=0)) + monkeypatch.setattr(asyncio, "create_subprocess_exec", spy) + + result = await get_ruff_config_markdown("tool.ruff.lint.select") + assert result == "lint.select docs" + assert spy.called_args[0] == ("ruff", "config", "lint.select") + + assert await get_ruff_config_markdown("tool.ruff") is None + + +@pytest.mark.asyncio +async def test_get_all_ruff_rules(monkeypatch: pytest.MonkeyPatch) -> None: + rules_json = b'[{"code": "RUF012", "name": "mutable-class-default"}]' + spy = SubprocessSpy(FakeProcess(stdout=rules_json, returncode=0)) + monkeypatch.setattr(asyncio, "create_subprocess_exec", spy) + + rules = await get_all_ruff_rules() + assert len(rules) == 1 + assert rules[0]["code"] == "RUF012" + + +@pytest.mark.asyncio +async def test_get_ruff_linters(monkeypatch: pytest.MonkeyPatch) -> None: + linters_json = b'[{"name": "pyflakes", "prefix": "F"}]' + spy = SubprocessSpy(FakeProcess(stdout=linters_json, returncode=0)) + monkeypatch.setattr(asyncio, "create_subprocess_exec", spy) + + linters = await get_ruff_linters() + assert len(linters) == 1 + assert linters[0]["name"] == "pyflakes" + + +def test_compute_effective_rules() -> None: + """Verify that compute_effective_rules computes Enabled, Ignored, and Disabled statuses.""" + all_rules: list[RuffRule] = [ + { + "code": "RUF012", + "name": "mutable-class-default", + "linter": "ruff", + "summary": "Mutable class default", + }, + { + "code": "F401", + "name": "unused-import", + "linter": "pyflakes", + "summary": "Unused import", + }, + { + "code": "E501", + "name": "line-too-long", + "linter": "pycodestyle", + "summary": "Line too long", + }, + ] + config = { + "tool": { + "ruff": { + "lint": { + "select": ["RUF"], + "ignore": ["RUF012"], + } + } + } + } + effective = compute_effective_rules(all_rules, config) + statuses = {r["code"]: r.get("status") for r in effective} + assert statuses["RUF012"] == "Ignored" + assert statuses["F401"] == "Disabled" + + +if __name__ == "__main__": + pytest.main([__file__, "-vv"]) diff --git a/tests/tui/test_tui.py b/tests/tui/test_tui.py index 6a764cb3..e04c577d 100644 --- a/tests/tui/test_tui.py +++ b/tests/tui/test_tui.py @@ -2,13 +2,13 @@ import asyncio from typing import TYPE_CHECKING, Any, cast -from unittest.mock import patch import pytest from textual.widgets import DataTable, Tree from ruff_sync.tui.app import RuffSyncApp from ruff_sync.tui.screens import LegendScreen +from ruff_sync.tui.types_ import LinterNode from ruff_sync.tui.widgets import CategoryTable, RuleInspector if TYPE_CHECKING: @@ -16,7 +16,7 @@ from ruff_sync.cli import Arguments from ruff_sync.tui.widgets import ConfigTree - from ruff_sync.types_ import RuffRule + from ruff_sync.types_ import RuffLinter, RuffRule from tests.conftest import CLIRunner @@ -123,7 +123,9 @@ async def test_ruff_sync_app_node_selection(mock_args: Arguments, tmp_path: path @pytest.mark.asyncio -async def test_ruff_sync_app_rule_selection(mock_args: Arguments, tmp_path: pathlib.Path) -> None: +async def test_ruff_sync_app_rule_selection( + mock_args: Arguments, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +) -> None: pyproject = tmp_path / "pyproject.toml" pyproject.write_text( """ @@ -136,50 +138,54 @@ async def test_ruff_sync_app_rule_selection(mock_args: Arguments, tmp_path: path app = RuffSyncApp(mock_args) mock_markdown = "## RUF012 Documentation\n\nDetailed info here." - with patch("ruff_sync.tui.widgets.get_ruff_rule_markdown", return_value=mock_markdown): - async with app.run_test() as pilot: - # Wait for background worker and tree repopulation (priming) - while not app.effective_rules: - await asyncio.sleep(0.1) - await pilot.pause() - - tree = app.query_one(Tree) - # Find and select RUF012 node in the now-stable tree - # It's inside tool.ruff -> lint -> select -> RUF012 - lint_node = next( - n - for n in tree.root.children - if str(n.label.plain if hasattr(n.label, "plain") else n.label) == "lint" - ) - lint_node.expand() - await pilot.pause() + async def fake_get_rule_markdown(code: str) -> str: + return mock_markdown - select_node = next( - n - for n in lint_node.children - if str(n.label.plain if hasattr(n.label, "plain") else n.label) == "select" - ) - select_node.expand() + monkeypatch.setattr("ruff_sync.tui.widgets.get_ruff_rule_markdown", fake_get_rule_markdown) + + async with app.run_test() as pilot: + # Wait for background worker and tree repopulation (priming) + while not app.effective_rules: + await asyncio.sleep(0.1) await pilot.pause() - rule_node = next( - n - for n in select_node.children - if str(n.label.plain if hasattr(n.label, "plain") else n.label) == "RUF012" - ) - tree.focus() - tree.select_node(rule_node) - await pilot.press("enter") + tree = app.query_one(Tree) + # Find and select RUF012 node in the now-stable tree + # It's inside tool.ruff -> lint -> select -> RUF012 + lint_node = next( + n + for n in tree.root.children + if str(n.label.plain if hasattr(n.label, "plain") else n.label) == "lint" + ) + lint_node.expand() + await pilot.pause() + + select_node = next( + n + for n in lint_node.children + if str(n.label.plain if hasattr(n.label, "plain") else n.label) == "select" + ) + select_node.expand() + await pilot.pause() + + rule_node = next( + n + for n in select_node.children + if str(n.label.plain if hasattr(n.label, "plain") else n.label) == "RUF012" + ) + tree.focus() + tree.select_node(rule_node) + await pilot.press("enter") - inspector = app.query_one("#inspector", RuleInspector) - # Wait for background worker and UI update - for _ in range(20): - await pilot.pause(0.2) - if "RUF012" in str(inspector.source): - break + inspector = app.query_one("#inspector", RuleInspector) + # Wait for background worker and UI update + for _ in range(20): + await pilot.pause(0.2) + if "RUF012" in str(inspector.source): + break - # Verify Markdown content (simplified check) - assert "RUF012" in str(inspector.source) + # Verify Markdown content (simplified check) + assert "RUF012" in str(inspector.source) def test_cli_inspect_subcommand( @@ -189,12 +195,17 @@ def test_cli_inspect_subcommand( # Mock load_local_ruff_config where it's used in RuffSyncApp.on_mount monkeypatch.setattr("ruff_sync.tui.app.load_local_ruff_config", lambda _: {}) - # Use patch to prevent the App from actually running (which would block/fail in CI) - # and just verify it was instantiated and run() was called. - with patch("ruff_sync.tui.app.RuffSyncApp.run", return_value=0) as mock_run: - exit_code, _out, _err = cli_run(["inspect", "--to", str(tmp_path)]) - assert exit_code == 0 - mock_run.assert_called_once() + run_calls: list[RuffSyncApp] = [] + + def fake_run_inspect(self: RuffSyncApp) -> int: + run_calls.append(self) + return 0 + + monkeypatch.setattr(RuffSyncApp, "run", fake_run_inspect) + + exit_code, _out, _err = cli_run(["inspect", "--to", str(tmp_path)]) + assert exit_code == 0 + assert len(run_calls) == 1 @pytest.mark.parametrize( @@ -223,37 +234,48 @@ def test_cli_ruff_inspect_entry_point_variations( if "--to" not in args and "--help" not in args: final_args = [*args, "--to", str(tmp_path)] - # 1. Test running (using patched run() to avoid TUI execution) - with patch("ruff_sync.tui.app.RuffSyncApp.run", return_value=0) as mock_run: - exit_code, _out, _err = cli_run(final_args, entry_point="ruff-inspect") - - # If --help was passed, argparse will exit 0 and not call run() - if "--help" in args: - assert exit_code == 0 - mock_run.assert_not_called() - elif expected_command == "inspect": - assert exit_code == 0 - mock_run.assert_called_once() - else: - # For 'check', asyncio.run(check()) is called, not RuffSyncApp.run() - assert exit_code == 0 - mock_run.assert_not_called() + # 1. Test running (using monkeypatched run() to avoid TUI execution) + run_calls: list[RuffSyncApp] = [] + + def fake_run_variations(self: RuffSyncApp) -> int: + run_calls.append(self) + return 0 + + monkeypatch.setattr(RuffSyncApp, "run", fake_run_variations) + + async def fake_check(*_a: Any, **_kw: Any) -> int: + return 0 + + monkeypatch.setattr("ruff_sync.cli.check", fake_check) + + exit_code, _out, _err = cli_run(final_args, entry_point="ruff-inspect") + expected_runs = 1 if (expected_command == "inspect" and "--help" not in args) else 0 + assert exit_code == 0 + assert len(run_calls) == expected_runs # 2. Test instantiation (to verify the command was correctly resolved) - with ( - patch("ruff_sync.tui.app.RuffSyncApp.__init__", return_value=None) as mock_init, - patch("ruff_sync.cli.asyncio.run", return_value=0), - patch("ruff_sync.tui.app.RuffSyncApp.run", return_value=0), - ): - cli_run(final_args, entry_point="ruff-inspect") - - if "--help" not in args: - if expected_command == "inspect": - mock_init.assert_called_once() - exec_args = mock_init.call_args[0][0] - assert exec_args.command == "inspect" - else: - mock_init.assert_not_called() + init_args: list[Any] = [] + original_init = RuffSyncApp.__init__ + + def spy_init(self: Any, parsed_args: Any, *a: Any, **kw: Any) -> None: + init_args.append(parsed_args) + original_init(self, parsed_args, *a, **kw) + + def fake_asyncio_run(coro: Any, *a: Any, **kw: Any) -> int: + if asyncio.iscoroutine(coro): + coro.close() + return 0 + + monkeypatch.setattr(RuffSyncApp, "__init__", spy_init) + monkeypatch.setattr("ruff_sync.cli.asyncio.run", fake_asyncio_run) + + cli_run(final_args, entry_point="ruff-inspect") + + if "--help" not in args: + expected_inits = 1 if expected_command == "inspect" else 0 + assert len(init_args) == expected_inits + if expected_inits: + assert init_args[0].command == "inspect" @pytest.mark.asyncio @@ -271,21 +293,24 @@ async def test_ruff_sync_app_show_legend(mock_args: Arguments) -> None: @pytest.mark.asyncio -async def test_ruff_sync_app_copy_content(mock_args: Arguments) -> None: +async def test_ruff_sync_app_copy_content( + mock_args: Arguments, monkeypatch: pytest.MonkeyPatch +) -> None: """The inspector content should be copied to the clipboard when 'c' is pressed.""" app = RuffSyncApp(mock_args) - # Mock copy_to_clipboard on the app instance - with patch.object(RuffSyncApp, "copy_to_clipboard") as mock_copy: - async with app.run_test() as pilot: - # Manually update inspector to simulate a selected rule/config - inspector = app.query_one(RuleInspector) - inspector.update("Copied Content Test") - await pilot.pause() + copied_texts: list[str] = [] + monkeypatch.setattr(app, "copy_to_clipboard", copied_texts.append) - await pilot.press("c") - await pilot.pause() + async with app.run_test() as pilot: + # Manually update inspector to simulate a selected rule/config + inspector = app.query_one(RuleInspector) + inspector.update("Copied Content Test") + await pilot.pause() + + await pilot.press("c") + await pilot.pause() - mock_copy.assert_called_once_with("Copied Content Test") + assert copied_texts == ["Copied Content Test"] @pytest.mark.asyncio @@ -377,5 +402,39 @@ async def test_category_table_handles_ignored_status( assert warning_hex in fix_cell.lower() +@pytest.mark.asyncio +async def test_category_table_renders_codeless_linter_node( + mock_args: Arguments, tmp_path: pathlib.Path +) -> None: + """Verify that CategoryTable correctly renders codeless rules under a LinterNode.""" + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text("[tool.ruff]\n", encoding="utf-8") + + app = RuffSyncApp(mock_args) + async with app.run_test() as pilot: + table = app.query_one("#category-table", CategoryTable) + + linter: RuffLinter = {"name": "pedantic"} + test_rule: RuffRule = { + "code": None, + "name": "pytest-fixture-autouse", + "category": "pedantic", + "linter": None, + "summary": "Avoid using autouse=True", + "status": "Enabled", + "fix_availability": "None", + } + + linter_node = LinterNode(linter, [test_rule]) + table.render_node(linter_node) + await pilot.pause() + + assert table.row_count == 1 + row = table.get_row_at(0) + assert "-" in str(row[0]) # Code column fallback + assert "pytest-fixture-autouse" in str(row[1]) + assert "pedantic" in str(row[2]) + + if __name__ == "__main__": pytest.main([__file__, "-vv"]) diff --git a/tests/tui/test_tui_types.py b/tests/tui/test_tui_types.py index 149b817b..fee05a55 100644 --- a/tests/tui/test_tui_types.py +++ b/tests/tui/test_tui_types.py @@ -2,9 +2,16 @@ from __future__ import annotations -import pytest - -from ruff_sync.tui.types_ import DictNode, ListNode, ScalarNode, wrap_data +from ruff_sync.tui.types_ import ( + DictNode, + ListNode, + RuleNode, + ScalarNode, + _is_linter_active, + rule_matches_linter, + wrap_data, +) +from ruff_sync.types_ import RuffLinter, RuffRule def test_wrap_data_scalar() -> None: @@ -83,5 +90,74 @@ def test_rule_node_behavior() -> None: assert node.doc_target() == ("RUF012", "rule") -if __name__ == "__main__": - pytest.main([__file__, "-vv"]) +def test_rule_node_codeless() -> None: + """Test RuleNode when rule has no code.""" + rule: RuffRule = { + "code": None, + "name": "pytest-fixture-autouse", + "category": "pedantic", + "linter": None, + "summary": "Avoid using autouse=True", + } + node = RuleNode(rule) + assert node.key == "pytest-fixture-autouse" + assert node.path == "__rule__:pytest-fixture-autouse" + assert node.doc_target() == ("pytest-fixture-autouse", "rule") + + +def test_rule_matches_linter() -> None: + """Test rule_matches_linter across standard, prefix, and codeless/category rules.""" + standard_linter: RuffLinter = {"name": "Pyflakes", "prefix": "F"} + category_linter: RuffLinter = {"name": "pedantic"} + prefix_linter: RuffLinter = {"name": "pytest", "prefix": "PT"} + + standard_rule: RuffRule = { + "code": "F401", + "name": "unused-import", + "linter": "Pyflakes", + "summary": "Unused import", + } + codeless_rule: RuffRule = { + "code": None, + "name": "pytest-fixture-autouse", + "category": "pedantic", + "linter": None, + "summary": "Avoid using autouse=True", + } + + assert rule_matches_linter(standard_rule, standard_linter) is True + assert rule_matches_linter(standard_rule, category_linter) is False + + # Codeless rule matches by category + assert rule_matches_linter(codeless_rule, category_linter) is True + # Codeless rule matches by name starting with prefix or name + assert rule_matches_linter(codeless_rule, prefix_linter) is True + assert rule_matches_linter(codeless_rule, standard_linter) is False + + +def test_is_linter_active_codeless() -> None: + """Test _is_linter_active when effective rules contain active codeless rules.""" + linter: RuffLinter = {"name": "pedantic"} + disabled_rules: list[RuffRule] = [ + { + "code": None, + "name": "pytest-fixture-autouse", + "category": "pedantic", + "linter": None, + "summary": "Avoid using autouse=True", + "status": "Disabled", + } + ] + enabled_rules: list[RuffRule] = [ + { + "code": None, + "name": "pytest-fixture-autouse", + "category": "pedantic", + "linter": None, + "summary": "Avoid using autouse=True", + "status": "Enabled", + } + ] + + assert _is_linter_active(linter, disabled_rules) is False + assert _is_linter_active(linter, enabled_rules) is True diff --git a/uv.lock b/uv.lock index b0570485..e1d23183 100644 --- a/uv.lock +++ b/uv.lock @@ -1708,32 +1708,32 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" }, - { url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" }, - { url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" }, - { url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" }, - { url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" }, - { url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" }, - { url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" }, - { url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" }, - { url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" }, - { url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" }, - { url = "https://files.pythonhosted.org/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" }, +version = "0.16.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" }, + { url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" }, + { url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" }, + { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" }, + { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" }, + { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" }, + { url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" }, ] [[package]] name = "ruff-sync" -version = "0.1.9.dev0" +version = "0.1.9.dev1" source = { editable = "." } dependencies = [ { name = "httpx2" }, @@ -1800,7 +1800,7 @@ dev = [ { name = "pytest-textual-snapshot", specifier = ">=1.0.0" }, { name = "respx", specifier = ">=0.23.1" }, { name = "ruamel-yaml", specifier = ">=0.18.6" }, - { name = "ruff", specifier = ">=0.16.0" }, + { name = "ruff", specifier = ">=0.16.5" }, { name = "textual", specifier = ">=8.2.2" }, { name = "wily", specifier = ">=1.25.0" }, ]