Skip to content

Commit 4b23e98

Browse files
committed
tools/codeformat.py: Add formatter using uncrustify for C, black for Py.
This commit adds a tool, codeformat.py, which will reformat C and Python code to fit a certain style. By default the tool will reformat (almost) all the original (ie not 3rd-party) .c, .h and .py files in this repository. Passing filenames on the command-line to codeformat.py will reformat only those. Reformatting is done in-place. uncrustify is used for C reformatting, which is available for many platforms and can be easily built from source, see https://github.com/uncrustify/uncrustify. The configuration for uncrustify is also added in this commit and values are chosen to best match the existing code style. A small post-processing stage on .c and .h files is done by codeformat.py (after running uncrustify) to fix up some minor items: - space inserted after * when used as multiplication with sizeof - #if/ifdef/ifndef/elif/else/endif are dedented by one level when they are configuring if-blocks and case-blocks. For Python code, the formatter used is black, which can be pip-installed; see https://github.com/psf/black. The defaults are used, except for line- length which is set at 99 characters to match the "about 100" line-length limit used in C code. The formatting tools used and their configuration were chosen to strike a balance between keeping existing style and not changing too many lines of code, and enforcing a relatively strict style (especially for Python code). This should help to keep the code consistent across everything, and reduce cognitive load when writing new code to match the style.
1 parent b169904 commit 4b23e98

File tree

2 files changed

+3167
-0
lines changed

2 files changed

+3167
-0
lines changed

tools/codeformat.py

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env python3
2+
#
3+
# This file is part of the MicroPython project, http://micropython.org/
4+
#
5+
# The MIT License (MIT)
6+
#
7+
# Copyright (c) 2020 Damien P. George
8+
# Copyright (c) 2020 Jim Mussared
9+
#
10+
# Permission is hereby granted, free of charge, to any person obtaining a copy
11+
# of this software and associated documentation files (the "Software"), to deal
12+
# in the Software without restriction, including without limitation the rights
13+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
# copies of the Software, and to permit persons to whom the Software is
15+
# furnished to do so, subject to the following conditions:
16+
#
17+
# The above copyright notice and this permission notice shall be included in
18+
# all copies or substantial portions of the Software.
19+
#
20+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26+
# THE SOFTWARE.
27+
28+
import argparse
29+
import glob
30+
import itertools
31+
import os
32+
import re
33+
import subprocess
34+
35+
# Relative to top-level repo dir.
36+
PATHS = [
37+
# C
38+
"extmod/*.[ch]",
39+
"lib/netutils/*.[ch]",
40+
"lib/timeutils/*.[ch]",
41+
"lib/utils/*.[ch]",
42+
"mpy-cross/*.[ch]",
43+
"ports/*/*.[ch]",
44+
"py/*.[ch]",
45+
# Python
46+
"drivers/**/*.py",
47+
"examples/**/*.py",
48+
"extmod/**/*.py",
49+
"ports/**/*.py",
50+
"py/**/*.py",
51+
"tools/**/*.py",
52+
]
53+
54+
EXCLUSIONS = [
55+
# STM32 build includes generated Python code.
56+
"ports/*/build*",
57+
# gitignore in ports/unix ignores *.py, so also do it here.
58+
"ports/unix/*.py",
59+
]
60+
61+
# Path to repo top-level dir.
62+
TOP = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
63+
64+
UNCRUSTIFY_CFG = os.path.join(TOP, "tools/uncrustify.cfg")
65+
66+
C_EXTS = (
67+
".c",
68+
".h",
69+
)
70+
PY_EXTS = (".py",)
71+
72+
73+
FIXUP_REPLACEMENTS = (
74+
(re.compile("sizeof\(([a-z_]+)\) \*\(([a-z_]+)\)"), r"sizeof(\1) * (\2)"),
75+
(re.compile("([0-9]+) \*sizeof"), r"\1 * sizeof"),
76+
)
77+
78+
79+
def list_files(paths, exclusions=None, prefix=""):
80+
files = set()
81+
for pattern in paths:
82+
files.update(glob.glob(os.path.join(prefix, pattern), recursive=True))
83+
for pattern in exclusions or []:
84+
files.difference_update(glob.fnmatch.filter(files, os.path.join(prefix, pattern)))
85+
return sorted(files)
86+
87+
88+
def fixup_c(filename):
89+
# Read file.
90+
with open(filename) as f:
91+
lines = f.readlines()
92+
93+
# Write out file with fixups.
94+
with open(filename, "w", newline="") as f:
95+
dedent_stack = []
96+
while lines:
97+
# Get next line.
98+
l = lines.pop(0)
99+
100+
# Dedent #'s to match indent of following line (not previous line).
101+
m = re.match(r"( +)#(if |ifdef |ifndef |elif |else|endif)", l)
102+
if m:
103+
indent = len(m.group(1))
104+
directive = m.group(2)
105+
if directive in ("if ", "ifdef ", "ifndef "):
106+
l_next = lines[0]
107+
indent_next = len(re.match(r"( *)", l_next).group(1))
108+
if indent - 4 == indent_next and re.match(r" +(} else |case )", l_next):
109+
# This #-line (and all associated ones) needs dedenting by 4 spaces.
110+
l = l[4:]
111+
dedent_stack.append(indent - 4)
112+
else:
113+
# This #-line does not need dedenting.
114+
dedent_stack.append(-1)
115+
else:
116+
if dedent_stack[-1] >= 0:
117+
# This associated #-line needs dedenting to match the #if.
118+
indent_diff = indent - dedent_stack[-1]
119+
assert indent_diff >= 0
120+
l = l[indent_diff:]
121+
if directive == "endif":
122+
dedent_stack.pop()
123+
124+
# Apply general regex-based fixups.
125+
for regex, replacement in FIXUP_REPLACEMENTS:
126+
l = regex.sub(replacement, l)
127+
128+
# Write out line.
129+
f.write(l)
130+
131+
assert not dedent_stack, filename
132+
133+
134+
def main():
135+
cmd_parser = argparse.ArgumentParser(description="Auto-format C and Python files.")
136+
cmd_parser.add_argument("-c", action="store_true", help="Format C code only")
137+
cmd_parser.add_argument("-p", action="store_true", help="Format Python code only")
138+
cmd_parser.add_argument("files", nargs="*", help="Run on specific globs")
139+
args = cmd_parser.parse_args()
140+
141+
# Setting only one of -c or -p disables the other. If both or neither are set, then do both.
142+
format_c = args.c or not args.p
143+
format_py = args.p or not args.c
144+
145+
# Expand the globs passed on the command line, or use the default globs above.
146+
files = []
147+
if args.files:
148+
files = list_files(args.files)
149+
else:
150+
files = list_files(PATHS, EXCLUSIONS, TOP)
151+
152+
# Extract files matching a specific language.
153+
def lang_files(exts):
154+
for file in files:
155+
if os.path.splitext(file)[1].lower() in exts:
156+
yield file
157+
158+
# Run tool on N files at a time (to avoid making the command line too long).
159+
def batch(cmd, files, N=200):
160+
while True:
161+
file_args = list(itertools.islice(files, N))
162+
if not file_args:
163+
break
164+
subprocess.check_call(cmd + file_args)
165+
166+
# Format C files with uncrustify.
167+
if format_c:
168+
batch(["uncrustify", "-c", UNCRUSTIFY_CFG, "-lC", "--no-backup"], lang_files(C_EXTS))
169+
for file in lang_files(C_EXTS):
170+
fixup_c(file)
171+
172+
# Format Python files with black.
173+
if format_py:
174+
batch(["black", "-q", "--fast", "--line-length=99"], lang_files(PY_EXTS))
175+
176+
177+
if __name__ == "__main__":
178+
main()

0 commit comments

Comments
 (0)