1 # Copyright 2015 Google Inc. All Rights Reserved.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
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.
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.
26 If no filenames are specified, YAPF reads the code from stdin.
36 from importlib_metadata
import metadata
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
43 __version__
= metadata('yapf')['Version']
47 wrapper
= io
.TextIOWrapper(sys
.stdin
.buffer, encoding
='utf-8')
48 return wrapper
.buffer.raw
.readall().decode('utf-8')
51 def _removeBOM(source
):
52 """Remove any Byte-order-Mark bytes from the beginning of a file."""
54 bom
= bom
.decode('utf-8')
55 if source
.startswith(bom
):
56 return source
[len(bom
):]
64 argv: command-line arguments, such as sys.argv (including the program name
68 Zero on successful program termination, non-zero otherwise.
69 With --diff: zero if there were no changes, non-zero otherwise.
72 YapfError: if none of the supplied files were Python files.
74 parser
= _BuildParser()
75 args
= parser
.parse_args(argv
[1:])
76 style_config
= args
.style
82 if args
.lines
and len(args
.files
) > 1:
83 parser
.error('cannot use -l/--lines with more than one file')
85 lines
= _GetLines(args
.lines
) if args
.lines
is not None else None
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 '
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
:
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())
106 except KeyboardInterrupt:
109 if style_config
is None and not args
.no_local_style
:
110 style_config
= file_resources
.GetDefaultStyleForDir(os
.getcwd())
112 source
= [line
.rstrip() for line
in original_source
]
113 source
[0] = _removeBOM(source
[0])
116 reformatted_source
, _
= yapf_api
.FormatCode(
117 str('\n'.join(source
).replace('\r\n', '\n') + '\n'),
119 style_config
=style_config
,
121 except errors
.YapfError
:
123 except Exception as e
:
124 raise errors
.YapfError(errors
.FormatErrorMsg(e
))
126 file_resources
.WriteReformattedCode('<stdout>', reformatted_source
)
129 # Get additional exclude patterns from ignorefile
130 exclude_patterns_from_ignore_file
= file_resources
.GetExcludePatternsForDir(
133 files
= file_resources
.GetCommandLineFiles(args
.files
, args
.recursive
,
134 (args
.exclude
or []) +
135 exclude_patterns_from_ignore_file
)
137 raise errors
.YapfError('input filenames did not match any python files')
139 changed
= FormatFiles(
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
,
148 verbose
=args
.verbose
,
149 print_modified
=args
.print_modified
)
150 return 1 if changed
and (args
.diff
or args
.quiet
) else 0
153 def _PrintHelp(args
):
154 """Prints the help menu."""
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
))
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
='')
170 def FormatFiles(filenames
,
173 no_local_style
=False,
179 print_modified
=False):
180 """Format a list of files.
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.
200 True if the source code changed in any of the files being formatted.
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
:
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
213 for future
in concurrent
.futures
.as_completed(future_formats
):
214 changed |
= future
.result()
216 for filename
in filenames
:
217 changed |
= _FormatFile(filename
, lines
, style_config
, no_local_style
,
218 in_place
, print_diff
, quiet
, verbose
,
223 def _FormatFile(filename
,
226 no_local_style
=False,
231 print_modified
=False):
232 """Format an individual file."""
233 if verbose
and not quiet
:
234 print(f
'Reformatting {filename}')
236 if style_config
is None and not no_local_style
:
237 style_config
= file_resources
.GetDefaultStyleForDir(
238 os
.path
.dirname(filename
))
241 reformatted_code
, encoding
, has_change
= yapf_api
.FormatFile(
244 style_config
=style_config
,
246 print_diff
=print_diff
,
247 logger
=logging
.warning
)
248 except errors
.YapfError
:
250 except Exception as e
:
251 raise errors
.YapfError(errors
.FormatErrorMsg(e
))
253 if not in_place
and not quiet
and reformatted_code
:
254 file_resources
.WriteReformattedCode(filename
, reformatted_code
, encoding
,
256 if print_modified
and has_change
and in_place
and not quiet
:
257 print(f
'Formatted {filename}')
261 def _GetLines(line_strings
):
262 """Parses the start and end lines from a line string like 'start-end'.
265 line_strings: (array of string) A list of strings representing a line
266 range like 'start-end'.
269 A list of tuples of the start and end line numbers.
272 ValueError: If the line string failed to parse or was an invalid line range.
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)))
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
))
287 """Constructs the parser for the command line arguments.
290 An ArgumentParser instance for the CLI.
292 parser
= argparse
.ArgumentParser(
293 prog
='yapf', description
='Formatter for Python code.')
298 version
='%(prog)s {}'.format(__version__
))
300 diff_inplace_quiet_group
= parser
.add_mutually_exclusive_group()
301 diff_inplace_quiet_group
.add_argument(
305 help='print the diff for the fixed source')
306 diff_inplace_quiet_group
.add_argument(
310 help='make changes to files in place')
311 diff_inplace_quiet_group
.add_argument(
315 help='output nothing and set return value')
317 lines_recursive_group
= parser
.add_mutually_exclusive_group()
318 lines_recursive_group
.add_argument(
322 help='run recursively over directories')
323 lines_recursive_group
.add_argument(
329 help='range of lines to reformat, one-based')
337 help='patterns for files to exclude from formatting')
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
)))
350 help=('show style settings and exit; this output can be '
351 'saved to .style.yapf to make your settings '
356 help="don't search for local style definition")
361 help=('run YAPF in parallel when formatting multiple files.'))
366 help='print out file names of modified files')
371 help='print out file names while processing')
374 'files', nargs
='*', help='reads from stdin when no files are specified.')
378 def run_main(): # pylint: disable=invalid-name
380 sys
.exit(main(sys
.argv
))
381 except errors
.YapfError
as e
:
382 sys
.stderr
.write('yapf: ' + str(e
) + '\n')
386 if __name__
== '__main__':