• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2015-2017 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
16YAPF uses the algorithm in clang-format to figure out the "best" formatting for
17Python code. It looks at the program as a series of "unwrappable lines" ---
18i.e., lines which, if there were no column limit, we would place all tokens on
19that line. It then uses a priority queue to figure out what the best formatting
20is --- i.e., the formatting with the least penalty.
21
22It differs from tools like autopep8 and pep8ify in that it doesn't just look for
23violations of the style guide, but looks at the module as a whole, making
24formatting decisions based on what's the best format for each line.
25
26If no filenames are specified, YAPF reads the code from stdin.
27"""
28from __future__ import print_function
29
30import argparse
31import logging
32import os
33import sys
34
35from yapf.yapflib import errors
36from yapf.yapflib import file_resources
37from yapf.yapflib import py3compat
38from yapf.yapflib import style
39from yapf.yapflib import yapf_api
40
41__version__ = '0.16.2'
42
43
44def main(argv):
45  """Main program.
46
47  Arguments:
48    argv: command-line arguments, such as sys.argv (including the program name
49      in argv[0]).
50
51  Returns:
52    0 if there were no changes, non-zero otherwise.
53
54  Raises:
55    YapfError: if none of the supplied files were Python files.
56  """
57  parser = argparse.ArgumentParser(description='Formatter for Python code.')
58  parser.add_argument(
59      '-v',
60      '--version',
61      action='store_true',
62      help='show version number and exit')
63
64  diff_inplace_group = parser.add_mutually_exclusive_group()
65  diff_inplace_group.add_argument(
66      '-d',
67      '--diff',
68      action='store_true',
69      help='print the diff for the fixed source')
70  diff_inplace_group.add_argument(
71      '-i',
72      '--in-place',
73      action='store_true',
74      help='make changes to files in place')
75
76  lines_recursive_group = parser.add_mutually_exclusive_group()
77  lines_recursive_group.add_argument(
78      '-r',
79      '--recursive',
80      action='store_true',
81      help='run recursively over directories')
82  lines_recursive_group.add_argument(
83      '-l',
84      '--lines',
85      metavar='START-END',
86      action='append',
87      default=None,
88      help='range of lines to reformat, one-based')
89
90  parser.add_argument(
91      '-e',
92      '--exclude',
93      metavar='PATTERN',
94      action='append',
95      default=None,
96      help='patterns for files to exclude from formatting')
97  parser.add_argument(
98      '--style',
99      action='store',
100      help=('specify formatting style: either a style name (for example "pep8" '
101            'or "google"), or the name of a file with style settings. The '
102            'default is pep8 unless a %s or %s file located in one of the '
103            'parent directories of the source file (or current directory for '
104            'stdin)' % (style.LOCAL_STYLE, style.SETUP_CONFIG)))
105  parser.add_argument(
106      '--style-help',
107      action='store_true',
108      help=('show style settings and exit; this output can be '
109            'saved to .style.yapf to make your settings '
110            'permanent'))
111  parser.add_argument(
112      '--no-local-style',
113      action='store_true',
114      help="don't search for local style definition")
115  parser.add_argument('--verify', action='store_true', help=argparse.SUPPRESS)
116  parser.add_argument(
117      '-p',
118      '--parallel',
119      action='store_true',
120      help=('Run yapf in parallel when formatting multiple files. Requires '
121            'concurrent.futures in Python 2.X'))
122
123  parser.add_argument('files', nargs='*')
124  args = parser.parse_args(argv[1:])
125
126  if args.version:
127    print('yapf {}'.format(__version__))
128    return 0
129
130  if args.style_help:
131    style.SetGlobalStyle(style.CreateStyleFromConfig(args.style))
132    print('[style]')
133    for option, docstring in sorted(style.Help().items()):
134      for line in docstring.splitlines():
135        print('#', line and ' ' or '', line, sep='')
136      print(option.lower(), '=', style.Get(option), sep='')
137      print()
138    return 0
139
140  if args.lines and len(args.files) > 1:
141    parser.error('cannot use -l/--lines with more than one file')
142
143  lines = _GetLines(args.lines) if args.lines is not None else None
144  if not args.files:
145    # No arguments specified. Read code from stdin.
146    if args.in_place or args.diff:
147      parser.error('cannot use --in-place or --diff flags when reading '
148                   'from stdin')
149
150    original_source = []
151    while True:
152      try:
153        # Use 'raw_input' instead of 'sys.stdin.read', because otherwise the
154        # user will need to hit 'Ctrl-D' more than once if they're inputting
155        # the program by hand. 'raw_input' throws an EOFError exception if
156        # 'Ctrl-D' is pressed, which makes it easy to bail out of this loop.
157        original_source.append(py3compat.raw_input())
158      except EOFError:
159        break
160
161    style_config = args.style
162    if style_config is None and not args.no_local_style:
163      style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
164
165    source = [line.rstrip() for line in original_source]
166    reformatted_source, _ = yapf_api.FormatCode(
167        py3compat.unicode('\n'.join(source) + '\n'),
168        filename='<stdin>',
169        style_config=style_config,
170        lines=lines,
171        verify=args.verify)
172    file_resources.WriteReformattedCode('<stdout>', reformatted_source)
173    return 0
174
175  files = file_resources.GetCommandLineFiles(args.files, args.recursive,
176                                             args.exclude)
177  if not files:
178    raise errors.YapfError('Input filenames did not match any python files')
179
180  FormatFiles(
181      files,
182      lines,
183      style_config=args.style,
184      no_local_style=args.no_local_style,
185      in_place=args.in_place,
186      print_diff=args.diff,
187      verify=args.verify,
188      parallel=args.parallel)
189  return 0
190
191
192def FormatFiles(filenames,
193                lines,
194                style_config=None,
195                no_local_style=False,
196                in_place=False,
197                print_diff=False,
198                verify=True,
199                parallel=False):
200  """Format a list of files.
201
202  Arguments:
203    filenames: (list of unicode) A list of files to reformat.
204    lines: (list of tuples of integers) A list of tuples of lines, [start, end],
205      that we want to format. The lines are 1-based indexed. This argument
206      overrides the 'args.lines'. It can be used by third-party code (e.g.,
207      IDEs) when reformatting a snippet of code.
208    style_config: (string) Style name or file path.
209    no_local_style: (string) If style_config is None don't search for
210      directory-local style configuration.
211    in_place: (bool) Modify the files in place.
212    print_diff: (bool) Instead of returning the reformatted source, return a
213      diff that turns the formatted source into reformatter source.
214    verify: (bool) True if reformatted code should be verified for syntax.
215    parallel: (bool) True if should format multiple files in parallel.
216
217  Returns:
218    True if the source code changed in any of the files being formatted.
219  """
220  changed = False
221  if parallel:
222    import multiprocessing  # pylint: disable=g-import-not-at-top
223    import concurrent.futures  # pylint: disable=g-import-not-at-top
224    workers = min(multiprocessing.cpu_count(), len(filenames))
225    with concurrent.futures.ProcessPoolExecutor(workers) as executor:
226      future_formats = [
227          executor.submit(_FormatFile, filename, lines, style_config,
228                          no_local_style, in_place, print_diff, verify)
229          for filename in filenames
230      ]
231      for future in concurrent.futures.as_completed(future_formats):
232        changed |= future.result()
233  else:
234    for filename in filenames:
235      changed |= _FormatFile(filename, lines, style_config, no_local_style,
236                             in_place, print_diff, verify)
237  return changed
238
239
240def _FormatFile(filename,
241                lines,
242                style_config=None,
243                no_local_style=False,
244                in_place=False,
245                print_diff=False,
246                verify=True):
247  logging.info('Reformatting %s', filename)
248  if style_config is None and not no_local_style:
249    style_config = (
250        file_resources.GetDefaultStyleForDir(os.path.dirname(filename)))
251  try:
252    reformatted_code, encoding, has_change = yapf_api.FormatFile(
253        filename,
254        in_place=in_place,
255        style_config=style_config,
256        lines=lines,
257        print_diff=print_diff,
258        verify=verify,
259        logger=logging.warning)
260    if not in_place and reformatted_code:
261      file_resources.WriteReformattedCode(filename, reformatted_code, in_place,
262                                          encoding)
263    return has_change
264  except SyntaxError as e:
265    e.filename = filename
266    raise
267
268
269def _GetLines(line_strings):
270  """Parses the start and end lines from a line string like 'start-end'.
271
272  Arguments:
273    line_strings: (array of string) A list of strings representing a line
274      range like 'start-end'.
275
276  Returns:
277    A list of tuples of the start and end line numbers.
278
279  Raises:
280    ValueError: If the line string failed to parse or was an invalid line range.
281  """
282  lines = []
283  for line_string in line_strings:
284    # The 'list' here is needed by Python 3.
285    line = list(map(int, line_string.split('-', 1)))
286    if line[0] < 1:
287      raise errors.YapfError('invalid start of line range: %r' % line)
288    if line[0] > line[1]:
289      raise errors.YapfError('end comes before start in line range: %r', line)
290    lines.append(tuple(line))
291  return lines
292
293
294def run_main():  # pylint: disable=invalid-name
295  try:
296    sys.exit(main(sys.argv))
297  except errors.YapfError as e:
298    sys.stderr.write('yapf: ' + str(e) + '\n')
299    sys.exit(1)
300
301
302if __name__ == '__main__':
303  run_main()
304