-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathpyck.py
More file actions
executable file
·292 lines (268 loc) · 12.4 KB
/
Copy pathpyck.py
File metadata and controls
executable file
·292 lines (268 loc) · 12.4 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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#!/usr/bin/env python
########################################################################
# pyck.py: Comprehensive Python Code Formatter and Linter
#
# Description:
# This script performs Python code auto-fixing and lint checking. It uses
# autoflake to remove unused imports, autopep8 to apply formatting fixes,
# isort to organize imports, and flake8 to detect lint issues.
#
# pyck deliberately separates changes that its automatic fixers can apply
# from lint findings that may require human judgment. "Would clean:",
# "Would format:", and "Would sort imports in:" are reserved for changes
# that pyck -i directly applies through autoflake, autopep8, and isort.
# flake8 findings are reported separately because flake8 can detect real
# defects that the configured automatic fixers cannot safely rewrite.
#
# Without -i, pyck performs a non-destructive dry run. flake8 findings are
# reported as "Lint issue (manual review candidate):". They are described
# as candidates at this stage because a later autoflake, autopep8, or isort
# change may indirectly eliminate the condition.
#
# With -i, pyck first runs autoflake, autopep8, and isort for each file,
# then runs flake8 against the resulting file. Any lint findings that remain
# are reported as "Manual fix required:". This means the configured
# automatic fixers have completed and the remaining finding requires human
# review.
#
# Remaining lint findings are advisory diagnostics, not pyck execution
# failures. A normally completed pyck run returns status 0 even when
# "Manual fix required:" findings remain. Existing non-zero statuses for
# actual execution failures are unchanged.
#
# pyck uses its own formatter and linter settings and ignores user-level and
# project-local configuration files. The same file is therefore checked and
# formatted with the same pyck policy regardless of the current working
# directory or configuration files surrounding the target.
#
# Author: id774 (More info: http://id774.net)
# Source Code: https://github.com/id774/scripts
# License: The GPL version 3, or LGPL version 3 (Dual License).
# Contact: idnanashi@gmail.com
#
# Usage:
# Without -i (Dry-run mode):
# pyck.py [file(s) or directory(ies)]
# Example:
# pyck.py ./my_python_project *.py
# This mode never modifies files. "Would clean:", "Would format:", and
# "Would sort imports in:" identify changes that -i would directly apply.
# flake8 findings are shown separately as
# "Lint issue (manual review candidate):" because they are not guaranteed
# to require manual correction until auto-fix has been applied.
#
# With -i (Actual formatting mode):
# pyck.py -i [file(s) or directory(ies)]
# Example:
# pyck.py -i ./my_python_project *.py
# For each file, this mode runs autoflake, autopep8, and isort, then runs
# flake8 on the resulting file. Remaining lint findings are reported as
# "Manual fix required:". These findings require human review but do not
# change the normal exit status from 0.
#
# Requirements:
# - Python Version: 3.2 or later
# - Dependencies: autopep8, flake8, autoflake, isort
#
# Version History:
# v3.0 2026-08-23
# Distinguish auto-fixable changes from lint findings, report
# unresolved lint issues after auto-fix, and use isolated formatter
# and linter configuration.
# v2.7 2026-07-15
# Quote file and directory paths before interpolating them into
# shell commands, to support paths containing spaces.
# v2.6 2025-07-01
# Standardized termination behavior for consistent script execution.
# v2.5 2025-06-23
# Unified usage output to display full script header and support common help/version options.
# v2.4 2025-04-14
# Unify error and info message formatting with stderr and prefix tags.
# v2.3 2024-01-28
# Replaced shutil.which with a custom which function to ensure compatibility
# with Python versions prior to 3.3.
# v2.2 2024-01-20
# Refactored to include a main function and separate argument parser setup function.
# v2.1 2024-01-18
# Added isort integration for organizing imports.
# Fixed TypeError in run_command function by decoding stdout to string.
# v2.0 2024-01-13
# Ported from shell script (pyck.sh) to Python (pyck.py) for enhanced
# portability and functionality.
# Integrated functionality of autopyck.sh, including dry-run mode.
# Added support for multiple files and directories, including wildcard usage.
# v1.4 2024-01-07
# Updated command existence and execution permission checks
# using a common function for enhanced reliability and maintainability.
# v1.3 2023-12-20
# Replaced 'which' with 'command -v' for command existence check.
# v1.2 2023-12-07
# Removed dependency on specific Python path.
# v1.1 2023-12-06
# Refactored for clarity, added detailed comments, and documentation.
# v1.0 2014-08-12
# Initial release.
#
########################################################################
import argparse
import glob
import os
import shlex
import subprocess
import sys
import tempfile
def usage():
""" Display the script header as usage information and exit. """
script_path = os.path.abspath(__file__)
in_header = False
try:
with open(script_path, 'r', encoding='utf-8') as f:
for line in f:
if line.strip().startswith('#' * 10):
if not in_header:
in_header = True
continue
else:
break
if in_header and line.startswith('#'):
if line.startswith('# '):
print(line[2:], end='')
else:
print(line[1:], end='')
except Exception as e:
print("Error reading usage information: %s" % str(e), file=sys.stderr)
sys.exit(1)
sys.exit(0)
def setup_argument_parser():
""" Initialize and return an argument parser for command-line options. """
parser = argparse.ArgumentParser(
description="Python Code Formatter and Linter")
parser.add_argument("paths", nargs='+', type=str,
help="Directories or files to format and lint")
parser.add_argument("-i", "--auto-fix",
action="store_true", help="Auto-fix code issues")
return parser
def find_command(cmd):
""" Check if a given command exists in the system's PATH. """
for path in os.environ["PATH"].split(os.pathsep):
full_path = os.path.join(path, cmd)
if os.path.isfile(full_path):
return full_path
return None
def check_command(cmd):
""" Verify if a command is available and executable in the system's PATH. """
cmd_path = find_command(cmd)
if not cmd_path:
# If the command is not found
print("[ERROR] Command '{}' is not installed. Please install {} and try again.".format(cmd, cmd), file=sys.stderr)
sys.exit(127)
elif not os.access(cmd_path, os.X_OK):
# If the command is found but not executable
print("[ERROR] Command '{}' is not executable. Please check the permissions.".format(cmd), file=sys.stderr)
sys.exit(126)
def create_isolated_config(directory):
""" Create an isolated shared configuration for formatter and linter tools. """
config_path = os.path.join(directory, 'pyck.cfg')
with open(config_path, 'w', encoding='utf-8') as f:
f.write(
"[autoflake]\n"
"quiet = false\n\n"
"[pycodestyle]\n\n"
"[isort]\n"
"lines_between_sections = 1\n"
)
return config_path
def format_imports(file_path, config_path):
""" Format and organize imports in a Python file using 'isort'. """
command = "isort --settings-path={} {}".format(
shlex.quote(config_path), shlex.quote(file_path))
subprocess.Popen(command, shell=True).wait()
def resolve_target_files(paths):
""" Resolve the given files/directories into the concrete list of .py files to process. """
target_files = []
for path in paths:
actual_path = path[0] if isinstance(path, list) else path
if os.path.isdir(actual_path):
for root, dirs, files in os.walk(actual_path):
for name in files:
if name.endswith('.py'):
target_files.append(os.path.join(root, name))
elif os.path.isfile(actual_path):
target_files.append(actual_path)
else:
print("[ERROR] The specified path '{}' is neither a file nor a directory.".format(
actual_path), file=sys.stderr)
return target_files
def dry_run_formatting(paths, ignore_errors, config_path):
""" Perform a dry run to show which files auto-fix would change, without making actual changes. """
print("[INFO] DRY RUN: No files will be modified. Use -i to auto-fix.")
for file_path in resolve_target_files(paths):
run_command(
"flake8 --isolated --ignore={} {}".format(
ignore_errors, shlex.quote(file_path)),
show_files="Lint issue (manual review candidate):")
run_command("autoflake --config={} --imports=django,requests,urllib3 --check {}".format(
shlex.quote(config_path), shlex.quote(file_path)),
show_files="Would clean: {}".format(file_path), literal_message=True)
run_command("autopep8 --global-config={} --ignore-local-config --ignore={} --diff --exit-code {}".format(
shlex.quote(config_path), ignore_errors, shlex.quote(file_path)),
show_files="Would format: {}".format(file_path), literal_message=True)
run_command("isort --settings-path={} --check-only {}".format(
shlex.quote(config_path), shlex.quote(file_path)),
show_files="Would sort imports in: {}".format(file_path), literal_message=True)
def execute_formatting(paths, ignore_errors, config_path):
""" Execute auto-formatting and report lint issues that remain afterward. """
for file_path in resolve_target_files(paths):
format_file(file_path, ignore_errors, config_path)
run_command(
"flake8 --isolated --ignore={} {}".format(
ignore_errors, shlex.quote(file_path)),
show_files="Manual fix required:")
def format_file(file_path, ignore_errors, config_path):
""" Format a single Python file by cleaning up imports, and applying 'autopep8' and 'isort'. """
command = "autoflake --config={} --imports=django,requests,urllib3 -i {}".format(
shlex.quote(config_path), shlex.quote(file_path))
subprocess.Popen(command, shell=True).wait()
command = "autopep8 --global-config={} --ignore-local-config --ignore={} -v -i {}".format(
shlex.quote(config_path), ignore_errors, shlex.quote(file_path))
subprocess.Popen(command, shell=True).wait()
format_imports(file_path, config_path)
def run_command(command, show_files=None, literal_message=False):
""" Execute a shell command and optionally display a message when it reports a non-zero exit status. """
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
stdout, _ = process.communicate()
if isinstance(stdout, bytes):
stdout = stdout.decode('utf-8')
if process.returncode != 0 and show_files:
if literal_message:
print(show_files)
else:
for line in stdout.split('\n'):
if line:
print("{} {}".format(show_files, line))
def main():
""" Parse command-line arguments and perform formatting or dry-run based on the input. """
parser = setup_argument_parser()
args = parser.parse_args()
expanded_paths = []
for path in args.paths:
expanded_paths.extend(glob.glob(path) or [path])
ignore_errors = "E302,E402,E501"
check_command("autopep8")
check_command("flake8")
check_command("autoflake")
check_command("isort")
with tempfile.TemporaryDirectory() as temp_dir:
config_path = create_isolated_config(temp_dir)
if args.auto_fix:
execute_formatting(expanded_paths, ignore_errors, config_path)
else:
dry_run_formatting(expanded_paths, ignore_errors, config_path)
return 0
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] in ('-h', '--help', '-v', '--version'):
usage()
if sys.version_info < (3, 2):
print("[ERROR] This script requires Python 3.2 or later.", file=sys.stderr)
sys.exit(9)
sys.exit(main())