]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | # Copyright 2015 Google Inc. All Rights Reserved. |
2 | # | |
3 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | # you may not use this file except in compliance with the License. | |
5 | # You may obtain a copy of the License at | |
6 | # | |
7 | # http://www.apache.org/licenses/LICENSE-2.0 | |
8 | # | |
9 | # Unless required by applicable law or agreed to in writing, software | |
10 | # distributed under the License is distributed on an "AS IS" BASIS, | |
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | # See the License for the specific language governing permissions and | |
13 | # limitations under the License. | |
14 | """YAPF. | |
15 | ||
16 | YAPF uses the algorithm in clang-format to figure out the "best" formatting for | |
17 | Python code. It looks at the program as a series of "unwrappable lines" --- | |
18 | i.e., lines which, if there were no column limit, we would place all tokens on | |
19 | that line. It then uses a priority queue to figure out what the best formatting | |
20 | is --- i.e., the formatting with the least penalty. | |
21 | ||
22 | It differs from tools like autopep8 in that it doesn't just look for | |
23 | violations of the style guide, but looks at the module as a whole, making | |
24 | formatting decisions based on what's the best format for each line. | |
25 | ||
26 | If no filenames are specified, YAPF reads the code from stdin. | |
27 | """ | |
28 | ||
29 | import argparse | |
30 | import codecs | |
31 | import io | |
32 | import logging | |
33 | import os | |
34 | import sys | |
35 | ||
36 | from importlib_metadata import metadata | |
37 | ||
38 | from yapf.yapflib import errors | |
39 | from yapf.yapflib import file_resources | |
40 | from yapf.yapflib import style | |
41 | from yapf.yapflib import yapf_api | |
42 | ||
43 | __version__ = metadata('yapf')['Version'] | |
44 | ||
45 | ||
46 | def _raw_input(): | |
47 | wrapper = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') | |
48 | return wrapper.buffer.raw.readall().decode('utf-8') | |
49 | ||
50 | ||
51 | def _removeBOM(source): | |
52 | """Remove any Byte-order-Mark bytes from the beginning of a file.""" | |
53 | bom = codecs.BOM_UTF8 | |
54 | bom = bom.decode('utf-8') | |
55 | if source.startswith(bom): | |
56 | return source[len(bom):] | |
57 | return source | |
58 | ||
59 | ||
60 | def main(argv): | |
61 | """Main program. | |
62 | ||
63 | Arguments: | |
64 | argv: command-line arguments, such as sys.argv (including the program name | |
65 | in argv[0]). | |
66 | ||
67 | Returns: | |
68 | Zero on successful program termination, non-zero otherwise. | |
69 | With --diff: zero if there were no changes, non-zero otherwise. | |
70 | ||
71 | Raises: | |
72 | YapfError: if none of the supplied files were Python files. | |
73 | """ | |
74 | parser = _BuildParser() | |
75 | args = parser.parse_args(argv[1:]) | |
76 | style_config = args.style | |
77 | ||
78 | if args.style_help: | |
79 | _PrintHelp(args) | |
80 | return 0 | |
81 | ||
82 | if args.lines and len(args.files) > 1: | |
83 | parser.error('cannot use -l/--lines with more than one file') | |
84 | ||
85 | lines = _GetLines(args.lines) if args.lines is not None else None | |
86 | if not args.files: | |
87 | # No arguments specified. Read code from stdin. | |
88 | if args.in_place or args.diff: | |
89 | parser.error('cannot use --in-place or --diff flags when reading ' | |
90 | 'from stdin') | |
91 | ||
92 | original_source = [] | |
93 | while True: | |
94 | # Test that sys.stdin has the "closed" attribute. When using pytest, it | |
95 | # co-opts sys.stdin, which makes the "main_tests.py" fail. This is gross. | |
96 | if hasattr(sys.stdin, 'closed') and sys.stdin.closed: | |
97 | break | |
98 | try: | |
99 | # Use 'raw_input' instead of 'sys.stdin.read', because otherwise the | |
100 | # user will need to hit 'Ctrl-D' more than once if they're inputting | |
101 | # the program by hand. 'raw_input' throws an EOFError exception if | |
102 | # 'Ctrl-D' is pressed, which makes it easy to bail out of this loop. | |
103 | original_source.append(_raw_input()) | |
104 | except EOFError: | |
105 | break | |
106 | except KeyboardInterrupt: | |
107 | return 1 | |
108 | ||
109 | if style_config is None and not args.no_local_style: | |
110 | style_config = file_resources.GetDefaultStyleForDir(os.getcwd()) | |
111 | ||
112 | source = [line.rstrip() for line in original_source] | |
113 | source[0] = _removeBOM(source[0]) | |
114 | ||
115 | try: | |
116 | reformatted_source, _ = yapf_api.FormatCode( | |
117 | str('\n'.join(source).replace('\r\n', '\n') + '\n'), | |
118 | filename='<stdin>', | |
119 | style_config=style_config, | |
120 | lines=lines) | |
121 | except errors.YapfError: | |
122 | raise | |
123 | except Exception as e: | |
124 | raise errors.YapfError(errors.FormatErrorMsg(e)) | |
125 | ||
126 | file_resources.WriteReformattedCode('<stdout>', reformatted_source) | |
127 | return 0 | |
128 | ||
129 | # Get additional exclude patterns from ignorefile | |
130 | exclude_patterns_from_ignore_file = file_resources.GetExcludePatternsForDir( | |
131 | os.getcwd()) | |
132 | ||
133 | files = file_resources.GetCommandLineFiles(args.files, args.recursive, | |
134 | (args.exclude or []) + | |
135 | exclude_patterns_from_ignore_file) | |
136 | if not files: | |
137 | raise errors.YapfError('input filenames did not match any python files') | |
138 | ||
139 | changed = FormatFiles( | |
140 | files, | |
141 | lines, | |
142 | style_config=args.style, | |
143 | no_local_style=args.no_local_style, | |
144 | in_place=args.in_place, | |
145 | print_diff=args.diff, | |
146 | parallel=args.parallel, | |
147 | quiet=args.quiet, | |
148 | verbose=args.verbose, | |
149 | print_modified=args.print_modified) | |
150 | return 1 if changed and (args.diff or args.quiet) else 0 | |
151 | ||
152 | ||
153 | def _PrintHelp(args): | |
154 | """Prints the help menu.""" | |
155 | ||
156 | if args.style is None and not args.no_local_style: | |
157 | args.style = file_resources.GetDefaultStyleForDir(os.getcwd()) | |
158 | style.SetGlobalStyle(style.CreateStyleFromConfig(args.style)) | |
159 | print('[style]') | |
160 | for option, docstring in sorted(style.Help().items()): | |
161 | for line in docstring.splitlines(): | |
162 | print('#', line and ' ' or '', line, sep='') | |
163 | option_value = style.Get(option) | |
164 | if isinstance(option_value, (set, list)): | |
165 | option_value = ', '.join(map(str, option_value)) | |
166 | print(option.lower(), '=', option_value, sep='') | |
167 | print() | |
168 | ||
169 | ||
170 | def FormatFiles(filenames, | |
171 | lines, | |
172 | style_config=None, | |
173 | no_local_style=False, | |
174 | in_place=False, | |
175 | print_diff=False, | |
176 | parallel=False, | |
177 | quiet=False, | |
178 | verbose=False, | |
179 | print_modified=False): | |
180 | """Format a list of files. | |
181 | ||
182 | Arguments: | |
183 | filenames: (list of unicode) A list of files to reformat. | |
184 | lines: (list of tuples of integers) A list of tuples of lines, [start, end], | |
185 | that we want to format. The lines are 1-based indexed. This argument | |
186 | overrides the 'args.lines'. It can be used by third-party code (e.g., | |
187 | IDEs) when reformatting a snippet of code. | |
188 | style_config: (string) Style name or file path. | |
189 | no_local_style: (string) If style_config is None don't search for | |
190 | directory-local style configuration. | |
191 | in_place: (bool) Modify the files in place. | |
192 | print_diff: (bool) Instead of returning the reformatted source, return a | |
193 | diff that turns the formatted source into reformatter source. | |
194 | parallel: (bool) True if should format multiple files in parallel. | |
195 | quiet: (bool) True if should output nothing. | |
196 | verbose: (bool) True if should print out filenames while processing. | |
197 | print_modified: (bool) True if should print out filenames of modified files. | |
198 | ||
199 | Returns: | |
200 | True if the source code changed in any of the files being formatted. | |
201 | """ | |
202 | changed = False | |
203 | if parallel: | |
204 | import concurrent.futures # pylint: disable=g-import-not-at-top | |
205 | import multiprocessing # pylint: disable=g-import-not-at-top | |
206 | workers = min(multiprocessing.cpu_count(), len(filenames)) | |
207 | with concurrent.futures.ProcessPoolExecutor(workers) as executor: | |
208 | future_formats = [ | |
209 | executor.submit(_FormatFile, filename, lines, style_config, | |
210 | no_local_style, in_place, print_diff, quiet, verbose, | |
211 | print_modified) for filename in filenames | |
212 | ] | |
213 | for future in concurrent.futures.as_completed(future_formats): | |
214 | changed |= future.result() | |
215 | else: | |
216 | for filename in filenames: | |
217 | changed |= _FormatFile(filename, lines, style_config, no_local_style, | |
218 | in_place, print_diff, quiet, verbose, | |
219 | print_modified) | |
220 | return changed | |
221 | ||
222 | ||
223 | def _FormatFile(filename, | |
224 | lines, | |
225 | style_config=None, | |
226 | no_local_style=False, | |
227 | in_place=False, | |
228 | print_diff=False, | |
229 | quiet=False, | |
230 | verbose=False, | |
231 | print_modified=False): | |
232 | """Format an individual file.""" | |
233 | if verbose and not quiet: | |
234 | print(f'Reformatting {filename}') | |
235 | ||
236 | if style_config is None and not no_local_style: | |
237 | style_config = file_resources.GetDefaultStyleForDir( | |
238 | os.path.dirname(filename)) | |
239 | ||
240 | try: | |
241 | reformatted_code, encoding, has_change = yapf_api.FormatFile( | |
242 | filename, | |
243 | in_place=in_place, | |
244 | style_config=style_config, | |
245 | lines=lines, | |
246 | print_diff=print_diff, | |
247 | logger=logging.warning) | |
248 | except errors.YapfError: | |
249 | raise | |
250 | except Exception as e: | |
251 | raise errors.YapfError(errors.FormatErrorMsg(e)) | |
252 | ||
253 | if not in_place and not quiet and reformatted_code: | |
254 | file_resources.WriteReformattedCode(filename, reformatted_code, encoding, | |
255 | in_place) | |
256 | if print_modified and has_change and in_place and not quiet: | |
257 | print(f'Formatted {filename}') | |
258 | return has_change | |
259 | ||
260 | ||
261 | def _GetLines(line_strings): | |
262 | """Parses the start and end lines from a line string like 'start-end'. | |
263 | ||
264 | Arguments: | |
265 | line_strings: (array of string) A list of strings representing a line | |
266 | range like 'start-end'. | |
267 | ||
268 | Returns: | |
269 | A list of tuples of the start and end line numbers. | |
270 | ||
271 | Raises: | |
272 | ValueError: If the line string failed to parse or was an invalid line range. | |
273 | """ | |
274 | lines = [] | |
275 | for line_string in line_strings: | |
276 | # The 'list' here is needed by Python 3. | |
277 | line = list(map(int, line_string.split('-', 1))) | |
278 | if line[0] < 1: | |
279 | raise errors.YapfError('invalid start of line range: %r' % line) | |
280 | if line[0] > line[1]: | |
281 | raise errors.YapfError('end comes before start in line range: %r' % line) | |
282 | lines.append(tuple(line)) | |
283 | return lines | |
284 | ||
285 | ||
286 | def _BuildParser(): | |
287 | """Constructs the parser for the command line arguments. | |
288 | ||
289 | Returns: | |
290 | An ArgumentParser instance for the CLI. | |
291 | """ | |
292 | parser = argparse.ArgumentParser( | |
293 | prog='yapf', description='Formatter for Python code.') | |
294 | parser.add_argument( | |
295 | '-v', | |
296 | '--version', | |
297 | action='version', | |
298 | version='%(prog)s {}'.format(__version__)) | |
299 | ||
300 | diff_inplace_quiet_group = parser.add_mutually_exclusive_group() | |
301 | diff_inplace_quiet_group.add_argument( | |
302 | '-d', | |
303 | '--diff', | |
304 | action='store_true', | |
305 | help='print the diff for the fixed source') | |
306 | diff_inplace_quiet_group.add_argument( | |
307 | '-i', | |
308 | '--in-place', | |
309 | action='store_true', | |
310 | help='make changes to files in place') | |
311 | diff_inplace_quiet_group.add_argument( | |
312 | '-q', | |
313 | '--quiet', | |
314 | action='store_true', | |
315 | help='output nothing and set return value') | |
316 | ||
317 | lines_recursive_group = parser.add_mutually_exclusive_group() | |
318 | lines_recursive_group.add_argument( | |
319 | '-r', | |
320 | '--recursive', | |
321 | action='store_true', | |
322 | help='run recursively over directories') | |
323 | lines_recursive_group.add_argument( | |
324 | '-l', | |
325 | '--lines', | |
326 | metavar='START-END', | |
327 | action='append', | |
328 | default=None, | |
329 | help='range of lines to reformat, one-based') | |
330 | ||
331 | parser.add_argument( | |
332 | '-e', | |
333 | '--exclude', | |
334 | metavar='PATTERN', | |
335 | action='append', | |
336 | default=None, | |
337 | help='patterns for files to exclude from formatting') | |
338 | parser.add_argument( | |
339 | '--style', | |
340 | action='store', | |
341 | help=('specify formatting style: either a style name (for example "pep8" ' | |
342 | 'or "google"), or the name of a file with style settings. The ' | |
343 | 'default is pep8 unless a %s or %s or %s file located in the same ' | |
344 | 'directory as the source or one of its parent directories ' | |
345 | '(for stdin, the current directory is used).' % | |
346 | (style.LOCAL_STYLE, style.SETUP_CONFIG, style.PYPROJECT_TOML))) | |
347 | parser.add_argument( | |
348 | '--style-help', | |
349 | action='store_true', | |
350 | help=('show style settings and exit; this output can be ' | |
351 | 'saved to .style.yapf to make your settings ' | |
352 | 'permanent')) | |
353 | parser.add_argument( | |
354 | '--no-local-style', | |
355 | action='store_true', | |
356 | help="don't search for local style definition") | |
357 | parser.add_argument( | |
358 | '-p', | |
359 | '--parallel', | |
360 | action='store_true', | |
361 | help=('run YAPF in parallel when formatting multiple files.')) | |
362 | parser.add_argument( | |
363 | '-m', | |
364 | '--print-modified', | |
365 | action='store_true', | |
366 | help='print out file names of modified files') | |
367 | parser.add_argument( | |
368 | '-vv', | |
369 | '--verbose', | |
370 | action='store_true', | |
371 | help='print out file names while processing') | |
372 | ||
373 | parser.add_argument( | |
374 | 'files', nargs='*', help='reads from stdin when no files are specified.') | |
375 | return parser | |
376 | ||
377 | ||
378 | def run_main(): # pylint: disable=invalid-name | |
379 | try: | |
380 | sys.exit(main(sys.argv)) | |
381 | except errors.YapfError as e: | |
382 | sys.stderr.write('yapf: ' + str(e) + '\n') | |
383 | sys.exit(1) | |
384 | ||
385 | ||
386 | if __name__ == '__main__': | |
387 | run_main() |