diff --git a/.claude/skills/lint/SKILL.md b/.claude/skills/lint/SKILL.md new file mode 100644 index 00000000..81237c7b --- /dev/null +++ b/.claude/skills/lint/SKILL.md @@ -0,0 +1,72 @@ +--- +name: lint +description: > + Run ruff lint checks on piker Python files for + docstring style (D2xx) and code convention + compliance. Checks staged files by default. +user-invocable: true +argument-hint: "[--all|--fix|--stats|paths...]" +allowed-tools: Bash(python3 *), Bash(ruff *), Read, Edit +--- + +# Ruff Lint Checker + +Run piker's ruff config against staged files, +specific paths, or the full codebase. + +## Available scripts + +- **`scripts/check.py`** — self-contained (PEP 723) + wrapper around `ruff check` that defaults to staged + files and uses the project's `ruff.toml`. + +## Usage + +```bash +# check staged Python files (default) +python3 scripts/check.py + +# check full codebase +python3 scripts/check.py --all + +# auto-fix fixable violations +python3 scripts/check.py --fix + +# preview fixes without applying +python3 scripts/check.py --diff + +# show per-rule violation counts +python3 scripts/check.py --stats + +# check specific files +python3 scripts/check.py piker/ui/_style.py +``` + +## Common violations + +| Rule | Meaning | Fixable? | +|------|---------|----------| +| D213 | summary not on 2nd line after `'''` | yes | +| D205 | no blank line after summary | no | +| D204 | no blank line after class docstring | yes | +| D209 | closing quotes not on own line | yes | +| D200 | ignored — piker always multiline | n/a | +| W291 | trailing whitespace | yes | + +## Fixing D213 (most common) + +Convert this: +```python +"""Summary on first line.""" +``` + +To piker's `'''` multiline style: +```python +''' +Summary on second line. + +''' +``` + +For D205, insert a blank line between the summary +and description paragraphs inside the docstring. diff --git a/.claude/skills/lint/scripts/check.py b/.claude/skills/lint/scripts/check.py new file mode 100755 index 00000000..d49ab370 --- /dev/null +++ b/.claude/skills/lint/scripts/check.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.12" +# dependencies = [] +# /// +''' +Ruff lint checker for piker docstring and code style. + +Checks staged files by default, or the full `piker/` +tree with `--all`. Uses the project's `ruff.toml`. + +''' +import argparse +import shutil +import subprocess +import sys + + +def get_staged_py_files() -> list[str]: + ''' + Return staged Python file paths (added/copied/modified). + + ''' + result = subprocess.run( + [ + 'git', 'diff', + '--cached', + '--name-only', + '--diff-filter=ACM', + '--', '*.py', + ], + capture_output=True, + text=True, + ) + return [ + f for f in result.stdout.strip().split('\n') + if f + ] + + +def main() -> int: + ''' + Parse args and run `ruff check` against targets. + + ''' + ap = argparse.ArgumentParser( + description=( + "Run ruff check with piker's ruff.toml config.\n" + '\n' + 'Default: checks staged .py files only.\n' + 'Use --all for the full piker/ tree.' + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + ap.add_argument( + 'paths', + nargs='*', + help='files/dirs to check (default: staged)', + ) + ap.add_argument( + '--all', '-a', + action='store_true', + dest='check_all', + help='check entire piker/ directory', + ) + ap.add_argument( + '--fix', + action='store_true', + help='auto-fix fixable violations', + ) + ap.add_argument( + '--diff', + action='store_true', + help='preview fixes as unified diff (dry-run)', + ) + ap.add_argument( + '--stats', + action='store_true', + help='show per-rule violation counts only', + ) + args = ap.parse_args() + + # determine check targets + if args.check_all: + targets: list[str] = ['piker/'] + elif args.paths: + targets = args.paths + else: + targets = get_staged_py_files() + if not targets: + print( + 'No staged Python files found.\n' + 'Use --all for full codebase ' + 'or pass file paths.' + ) + return 0 + print( + f'Checking {len(targets)} staged file(s)..\n' + ) + + # ensure ruff is available on PATH + if not shutil.which('ruff'): + print( + 'Error: `ruff` not found on PATH.\n' + 'On NixOS: add to flake.nix or ' + '`nix-shell -p ruff`\n' + 'Otherwise: `uv sync --group lint` or ' + '`pip install ruff`' + ) + return 127 + + # build ruff command + cmd: list[str] = ['ruff', 'check'] + + if args.diff: + cmd.append('--diff') + elif args.fix: + cmd.append('--fix') + + if args.stats: + cmd.append('--statistics') + + cmd.extend(targets) + + result = subprocess.run(cmd) + return result.returncode + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/ruff.toml b/ruff.toml index 01873dab..f4d289d7 100644 --- a/ruff.toml +++ b/ruff.toml @@ -35,7 +35,7 @@ exclude = [ line-length = 88 indent-width = 4 -# Assume Python 3.9 +# !XXX sync with `pyproject.toml`! target-version = "py312" # ------ - ------ @@ -44,11 +44,43 @@ target-version = "py312" [lint] -# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. -# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or -# McCabe complexity (`C901`) by default. -select = ["E4", "E7", "E9", "F"] -ignore = [] +select = [ + # pycodestyle error subsets (pre-existing) + "E4", "E7", "E9", + # pyflakes (pre-existing) + "F", + + # -- pydocstyle: enforce piker's ''' multiline style -- + # D2xx whitespace and formatting rules; most are + # auto-fixable via `ruff check --fix`. + # NOTE: D1xx (missing-docstring) rules intentionally + # excluded to avoid noise across the existing codebase. + "D2", + # D4xx content checks (cherry-picked) + "D402", # first line != function signature + "D403", # capitalize first word + "D419", # no empty docstrings + # NOTE: D3xx skipped entirely since D300 enforces + # triple-double-quotes `"""` which conflicts with + # piker's `'''` convention (the formatter's + # `quote-style = "single"` handles conversion). + + # pycodestyle warnings + "W", +] +ignore = [ + # -- pydocstyle ignores for piker conventions -- + # piker ALWAYS uses multiline docstring style, never + # single-line, so disable the "fit on one line" rule. + "D200", + # piker uses NO blank line before class docstrings + # (D211) not 1-blank-line (D203); these conflict. + "D203", + # piker puts the summary on the SECOND line after + # an opening `'''` (D213); not on the same line as + # the opening quotes (D212); these conflict. + "D212", +] ignore-init-module-imports = false [lint.per-file-ignores] @@ -79,16 +111,10 @@ 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 = false +# Auto-format code examples inside docstrings +# (>>> blocks, code fences, etc.) +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" +# Use piker's 67-char target for code inside docstrings +# (only applies when `docstring-code-format = true`). +docstring-code-line-length = 67