Skip to content

Commit 6b82469

Browse files
committed
add in-place CLI formatting with side-by-side diff option and ignore support
1 parent 6373e76 commit 6b82469

3 files changed

Lines changed: 109 additions & 1 deletion

File tree

‎style50/__main__.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,37 @@ def main():
3434
parser.add_argument("-o", "--output", action="store", default="character",
3535
choices=["character", "split", "unified", "score", "json", "html", "format"], metavar="MODE",
3636
help="output mode, which can be character (default), split, unified, score, json, html, or format")
37+
parser.add_argument("-y", "--side-by-side", action="store_true",
38+
help="show side-by-side diff (equivalent to -o split)")
39+
parser.add_argument("-i", "--in-place", action="store_true",
40+
help="rewrite files in place with style50 formatting")
3741
parser.add_argument("-v", "--verbose", action="store_true",
3842
help="print full tracebacks of errors")
3943
parser.add_argument("-V", "--version", action="version",
4044
version="%(prog)s {}".format(__version__))
4145
parser.add_argument("-E", "--extensions", action="version",
4246
version=json.dumps(list(Style50.extension_map.keys())),
4347
help="print supported file extensions (as JSON list) and exit")
44-
parser.add_argument("-i", "--ignore", action="append", metavar="PATTERN",
48+
parser.add_argument("--ignore", action="append", metavar="PATTERN",
4549
help="paths/patterns to be ignored")
4650
parser.add_argument("--clang-format-style", metavar="STYLE",
4751
help="clang-format style string or file:// URI (overrides default CS50 config)")
4852

4953
args = parser.parse_args()
5054
ignore = args.ignore or filter(None, os.getenv("STYLE50_IGNORE", "").split(","))
5155

56+
if args.in_place and args.side_by_side:
57+
sys.exit("--in-place cannot be combined with --side-by-side")
58+
59+
if args.side_by_side:
60+
args.output = "split"
61+
62+
if args.in_place:
63+
if args.output != "character":
64+
sys.exit("--in-place cannot be combined with --output")
65+
Style50("format", clang_format_style=args.clang_format_style).format_files_in_place(args.file, ignore=ignore)
66+
return
67+
5268
if args.output == "format":
5369
if len(args.file) != 1:
5470
sys.exit("format mode requires exactly one file")

‎style50/_api.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,32 @@ def format_file(self, path):
6060
results = self._check(path)
6161
return results.styled
6262

63+
def format_files_in_place(self, paths, ignore=[]):
64+
"""Format files and rewrite each path in place."""
65+
try:
66+
# Translate each ignore pattern into a regex and compile it
67+
ignore = [re.compile(fnmatch.translate(i)) for i in ignore]
68+
except re.error:
69+
raise Error("failed to parse ignore pattern")
70+
71+
files = list(filter(lambda p: not any(reg.match(p) for reg in ignore),
72+
itertools.chain.from_iterable([path] if not os.path.isdir(path)
73+
else (os.path.join(root, file)
74+
for root, _, files in os.walk(path)
75+
for file in files)
76+
for path in paths)))
77+
78+
for path in files:
79+
try:
80+
results = self._check(path)
81+
except Error as e:
82+
termcolor.cprint(f"{path}: {e.msg}", "red", file=sys.stderr)
83+
continue
84+
85+
if results.original != results.styled:
86+
with open(path, "w") as file:
87+
file.write(results.styled)
88+
6389
def run(self, *args, **kwargs):
6490
"""Wraps Style50.check and renders the results using the renderer determined by self.output"""
6591
if self.output == "format":

‎tests/test_cli.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,69 @@ def test_clang_format_style_arg_is_accepted_in_format_mode(tmp_path):
4646
assert proc.returncode == 0
4747
assert proc.stdout.strip() != ""
4848

49+
50+
def test_in_place_rewrites_file(tmp_path):
51+
f = tmp_path / "t.py"
52+
original = "x=1\nprint( x)\n"
53+
f.write_text(original, encoding="utf-8")
54+
55+
proc = run_style50(["-i", str(f)])
56+
assert proc.returncode == 0
57+
58+
updated = f.read_text(encoding="utf-8")
59+
assert updated != original
60+
assert "print(x)" in updated
61+
62+
63+
def test_in_place_multiple_files(tmp_path):
64+
f1 = tmp_path / "a.py"
65+
f2 = tmp_path / "b.py"
66+
f1.write_text("x=1\nprint( x)\n", encoding="utf-8")
67+
f2.write_text("y=2\nprint( y)\n", encoding="utf-8")
68+
69+
proc = run_style50(["-i", str(f1), str(f2)])
70+
assert proc.returncode == 0
71+
assert "print(x)" in f1.read_text(encoding="utf-8")
72+
assert "print(y)" in f2.read_text(encoding="utf-8")
73+
74+
75+
def test_in_place_skips_already_styled(tmp_path):
76+
f = tmp_path / "t.py"
77+
original = "x = 1\nprint(x)\n"
78+
f.write_text(original, encoding="utf-8")
79+
80+
proc = run_style50(["-i", str(f)])
81+
assert proc.returncode == 0
82+
assert f.read_text(encoding="utf-8") == original
83+
84+
85+
def test_side_by_side_flag(tmp_path):
86+
f = tmp_path / "t.py"
87+
f.write_text("x=1\nprint( x)\n", encoding="utf-8")
88+
89+
proc = run_style50(["-y", str(f)])
90+
assert proc.returncode == 0
91+
assert proc.stdout.strip() != ""
92+
93+
94+
def test_in_place_and_side_by_side_conflict(tmp_path):
95+
f = tmp_path / "t.py"
96+
f.write_text("x=1\nprint( x)\n", encoding="utf-8")
97+
98+
proc = run_style50(["-i", "-y", str(f)])
99+
assert proc.returncode != 0
100+
assert "--in-place cannot be combined with --side-by-side" in (proc.stdout + proc.stderr)
101+
102+
103+
def test_in_place_respects_ignore(tmp_path):
104+
keep = tmp_path / "keep.py"
105+
skip = tmp_path / "skip.py"
106+
keep.write_text("x=1\nprint( x)\n", encoding="utf-8")
107+
skip.write_text("y=1\nprint( y)\n", encoding="utf-8")
108+
109+
proc = run_style50(["-i", "--ignore", "*skip.py", str(keep), str(skip)])
110+
assert proc.returncode == 0
111+
112+
assert "print(x)" in keep.read_text(encoding="utf-8")
113+
assert "print( y)" in skip.read_text(encoding="utf-8")
114+

0 commit comments

Comments
 (0)