Skip to content

Commit d22d68a

Browse files
committed
respect kwargs spacing
1 parent 7ef1dbe commit d22d68a

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@ repos:
77
- --fix
88
- --exit-non-zero-on-fix
99
- id: ruff-format
10+
- repo: local
11+
hooks:
12+
- id: enforce-kwargs-spacing
13+
name: Enforce spaces around keyword equals
14+
entry: scripts/enforce_kwargs_spacing.py
15+
language: system
16+
types: [python]

scripts/enforce_kwargs_spacing.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env python3
2+
"""Ensure keyword arguments use spaces around '=' (e.g., foo = bar)."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import io
8+
import sys
9+
import tokenize
10+
from collections import defaultdict
11+
from pathlib import Path
12+
13+
14+
def enforce_spacing(text: str) -> tuple[str, bool]:
15+
"""Return updated text with keyword '=' padded by spaces, plus change flag."""
16+
lines = text.splitlines(keepends = True)
17+
if not lines:
18+
return text, False
19+
20+
offsets: dict[int, int] = defaultdict(int)
21+
changed = False
22+
23+
reader = io.StringIO(text).readline
24+
for token in tokenize.generate_tokens(reader):
25+
if token.type != tokenize.OP or token.string != "=":
26+
continue
27+
28+
line_index = token.start[0] - 1
29+
col = token.start[1] + offsets[line_index]
30+
31+
if line_index < 0 or line_index >= len(lines):
32+
continue
33+
34+
line = lines[line_index]
35+
if col >= len(line) or line[col] != "=":
36+
continue
37+
38+
line_changed = False
39+
40+
# Insert a space before '=' when missing and not preceded by whitespace.
41+
if col > 0 and line[col - 1] not in {" ", "\t"}:
42+
line = f"{line[:col]} {line[col:]}"
43+
offsets[line_index] += 1
44+
col += 1
45+
line_changed = True
46+
changed = True
47+
48+
# Insert a space after '=' when missing and not followed by whitespace or newline.
49+
next_index = col + 1
50+
if next_index < len(line) and line[next_index] not in {" ", "\t", "\n", "\r"}:
51+
line = f"{line[:next_index]} {line[next_index:]}"
52+
offsets[line_index] += 1
53+
line_changed = True
54+
changed = True
55+
56+
if line_changed:
57+
lines[line_index] = line
58+
59+
if not changed:
60+
return text, False
61+
62+
return "".join(lines), True
63+
64+
65+
def process_file(path: Path) -> bool:
66+
try:
67+
with tokenize.open(path) as handle:
68+
original = handle.read()
69+
encoding = handle.encoding
70+
except (OSError, SyntaxError) as exc: # SyntaxError from tokenize on invalid python
71+
print(f"Failed to read {path}: {exc}", file = sys.stderr)
72+
return False
73+
74+
updated, changed = enforce_spacing(original)
75+
if changed:
76+
path.write_text(updated, encoding = encoding)
77+
return changed
78+
79+
80+
def main(argv: list[str]) -> int:
81+
parser = argparse.ArgumentParser(description = __doc__)
82+
parser.add_argument("files", nargs = "+", help = "Python files to fix")
83+
args = parser.parse_args(argv)
84+
85+
touched: list[Path] = []
86+
for entry in args.files:
87+
path = Path(entry)
88+
if not path.exists() or path.is_dir():
89+
continue
90+
if process_file(path):
91+
touched.append(path)
92+
93+
if touched:
94+
for path in touched:
95+
print(f"Adjusted kwarg spacing in {path}")
96+
return 0
97+
98+
99+
if __name__ == "__main__":
100+
sys.exit(main(sys.argv[1:]))

0 commit comments

Comments
 (0)