-
Notifications
You must be signed in to change notification settings - Fork 42
Expand file tree
/
Copy pathformat.py
More file actions
162 lines (139 loc) · 5.83 KB
/
Copy pathformat.py
File metadata and controls
162 lines (139 loc) · 5.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import ast
import os
import sys
def get_definitions_to_format(code):
"""
Parse the code and return a list of (start_line, end_line) tuples for all
function definitions (including methods within classes).
Args:
code (str): The source code as a string.
Returns:
list[tuple[int, int]]: Sorted list of (start, end) line numbers for definitions.
"""
tree = ast.parse(code)
defs = []
for node in ast.walk(tree): # Use ast.walk to find all function definitions
if isinstance(node, ast.FunctionDef):
start = node.lineno
# Ensure end_lineno is captured correctly, may need adjustment for decorators
# Finding the true end might require iterating through node attributes or using get_source_segment
# For now, rely on ast's end_lineno which is usually sufficient
end = getattr(node, 'end_lineno', None)
if end is not None:
defs.append((start, end))
# Sort definitions by starting line number
defs.sort()
return defs
def reduce_blank_lines(lines):
"""
Reduce multiple consecutive blank lines to a single blank line, preserving leading whitespace.
Args:
lines (list[str]): List of lines from the file, with trailing whitespace already removed.
Returns:
list[str]: Processed lines with consecutive blanks reduced to one.
"""
result = []
for line in lines:
is_blank = line.strip() == ''
if not is_blank:
result.append(line)
return result
def remove_trailing_blanks(lines):
"""
Remove trailing blank lines from a list of lines.
Args:
lines (list[str]): List of lines.
Returns:
list[str]: Lines with trailing blanks removed.
"""
while lines and lines[-1].strip() == '':
lines.pop()
return lines
def format_file(file_path):
"""
Format a single Python file by adjusting blank lines and removing trailing whitespace.
Args:
file_path (str): Path to the Python file to format.
"""
# Read the file
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Parse the code to find all function/method definitions
code = ''.join(lines)
try:
defs = get_definitions_to_format(code) # Use the new function
except SyntaxError as e:
print(f"Syntax error in {file_path}: {e}")
sys.exit(1) # Exit the script if a syntax error occurs
new_lines = []
if defs:
# Process the segment before the first definition
before_lines = lines[:defs[0][0] - 1] # 0-based indexing
before_lines = remove_trailing_blanks([line.rstrip() for line in before_lines])
if before_lines: # Only add blank lines if there's code before
new_lines.extend(before_lines)
new_lines.extend(['', ''])
# Process each definition and the segments between them
for i in range(len(defs)):
start, end = defs[i]
def_lines = lines[start - 1:end] # Adjust for 0-based indexing
def_lines = reduce_blank_lines([line.rstrip() for line in def_lines])
new_lines.extend(def_lines)
if i < len(defs) - 1:
# Process the segment between this definition and the next
next_start = defs[i + 1][0]
between_lines = lines[end:next_start - 1]
between_lines = remove_trailing_blanks([line.rstrip() for line in between_lines])
if between_lines:
new_lines.extend(between_lines)
new_lines.extend(['', '']) # Two blank lines before next definition
# Process the segment after the last definition
after_lines = lines[defs[-1][1]:]
after_lines = remove_trailing_blanks([line.rstrip() for line in after_lines])
if after_lines:
new_lines.extend(after_lines)
else:
# No top-level definitions; just clean up the lines
new_lines = remove_trailing_blanks([line.rstrip() for line in lines])
# Write the formatted lines back to the file
with open(file_path, 'w', encoding='utf-8') as f:
for line in new_lines:
f.write(line + '\n')
def format_directory(directory):
"""
Format all Python files in the given directory and its subdirectories,
skipping directories that start with a dot and common virtual environment patterns.
Args:
directory (str): Path to the directory to process.
"""
# Common patterns to skip (venv, env, .venv, path/to/venv, etc.)
skip_patterns = ['.venv', 'venv', 'env', '.env', 'path']
for root, dirs, files in os.walk(directory):
# Skip directories starting with a dot
dirs[:] = [d for d in dirs if not d.startswith('.')]
# Skip common virtual environment directories
dirs[:] = [d for d in dirs if d.lower() not in skip_patterns]
# Skip if any parent directory matches skip patterns
if any(pattern in root.lower() for pattern in skip_patterns):
continue
for file in files:
if file.endswith('.py'):
file_path = os.path.join(root, file)
print(f"Formatting {file_path}")
format_file(file_path)
if __name__ == '__main__':
if len(sys.argv) != 2:
print("Usage: python format.py <file_or_directory>")
sys.exit(1)
path_arg = sys.argv[1]
if os.path.isfile(path_arg):
if path_arg.endswith('.py'):
print(f"Formatting {path_arg}")
format_file(path_arg)
else:
print(f"Skipping non-Python file: {path_arg}")
elif os.path.isdir(path_arg):
format_directory(path_arg)
else:
print(f"Error: '{path_arg}' is not a valid file or directory")
sys.exit(1)