aboutsummaryrefslogtreecommitdiff
path: root/yapf/__init__.py
blob: 22ab014c0b804be43690fc681ac864e4692f74ae (plain)
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""YAPF.

YAPF uses the algorithm in clang-format to figure out the "best" formatting for
Python code. It looks at the program as a series of "unwrappable lines" ---
i.e., lines which, if there were no column limit, we would place all tokens on
that line. It then uses a priority queue to figure out what the best formatting
is --- i.e., the formatting with the least penalty.

It differs from tools like autopep8 and pep8ify in that it doesn't just look for
violations of the style guide, but looks at the module as a whole, making
formatting decisions based on what's the best format for each line.

If no filenames are specified, YAPF reads the code from stdin.
"""
from __future__ import print_function

import argparse
import logging
import os
import sys

from yapf.yapflib import errors
from yapf.yapflib import file_resources
from yapf.yapflib import py3compat
from yapf.yapflib import style
from yapf.yapflib import yapf_api

__version__ = '0.22.0'


def main(argv):
  """Main program.

  Arguments:
    argv: command-line arguments, such as sys.argv (including the program name
      in argv[0]).

  Returns:
    Zero on successful program termination, non-zero otherwise.
    With --diff: zero if there were no changes, non-zero otherwise.

  Raises:
    YapfError: if none of the supplied files were Python files.
  """
  parser = argparse.ArgumentParser(description='Formatter for Python code.')
  parser.add_argument(
      '-v',
      '--version',
      action='store_true',
      help='show version number and exit')

  diff_inplace_group = parser.add_mutually_exclusive_group()
  diff_inplace_group.add_argument(
      '-d',
      '--diff',
      action='store_true',
      help='print the diff for the fixed source')
  diff_inplace_group.add_argument(
      '-i',
      '--in-place',
      action='store_true',
      help='make changes to files in place')

  lines_recursive_group = parser.add_mutually_exclusive_group()
  lines_recursive_group.add_argument(
      '-r',
      '--recursive',
      action='store_true',
      help='run recursively over directories')
  lines_recursive_group.add_argument(
      '-l',
      '--lines',
      metavar='START-END',
      action='append',
      default=None,
      help='range of lines to reformat, one-based')

  parser.add_argument(
      '-e',
      '--exclude',
      metavar='PATTERN',
      action='append',
      default=None,
      help='patterns for files to exclude from formatting')
  parser.add_argument(
      '--style',
      action='store',
      help=('specify formatting style: either a style name (for example "pep8" '
            'or "google"), or the name of a file with style settings. The '
            'default is pep8 unless a %s or %s file located in the same '
            'directory as the source or one of its parent directories '
            '(for stdin, the current directory is used).' %
            (style.LOCAL_STYLE, style.SETUP_CONFIG)))
  parser.add_argument(
      '--style-help',
      action='store_true',
      help=('show style settings and exit; this output can be '
            'saved to .style.yapf to make your settings '
            'permanent'))
  parser.add_argument(
      '--no-local-style',
      action='store_true',
      help="don't search for local style definition")
  parser.add_argument('--verify', action='store_true', help=argparse.SUPPRESS)
  parser.add_argument(
      '-p',
      '--parallel',
      action='store_true',
      help=('Run yapf in parallel when formatting multiple files. Requires '
            'concurrent.futures in Python 2.X'))
  parser.add_argument(
      '-vv',
      '--verbose',
      action='store_true',
      help='Print out file names while processing')

  parser.add_argument(
      'files', nargs='*', help='Reads from stdin when no files are specified.')
  args = parser.parse_args(argv[1:])

  if args.version:
    print('yapf {}'.format(__version__))
    return 0

  style_config = args.style

  if args.style_help:
    if style_config is None and not args.no_local_style:
      style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
    style.SetGlobalStyle(style.CreateStyleFromConfig(style_config))
    print('[style]')
    for option, docstring in sorted(style.Help().items()):
      for line in docstring.splitlines():
        print('#', line and ' ' or '', line, sep='')
      print(option.lower(), '=', style.Get(option), sep='')
      print()
    return 0

  if args.lines and len(args.files) > 1:
    parser.error('cannot use -l/--lines with more than one file')

  lines = _GetLines(args.lines) if args.lines is not None else None
  if not args.files:
    # No arguments specified. Read code from stdin.
    if args.in_place or args.diff:
      parser.error('cannot use --in-place or --diff flags when reading '
                   'from stdin')

    original_source = []
    while True:
      if sys.stdin.closed:
        break
      try:
        # Use 'raw_input' instead of 'sys.stdin.read', because otherwise the
        # user will need to hit 'Ctrl-D' more than once if they're inputting
        # the program by hand. 'raw_input' throws an EOFError exception if
        # 'Ctrl-D' is pressed, which makes it easy to bail out of this loop.
        original_source.append(py3compat.raw_input())
      except EOFError:
        break

    if style_config is None and not args.no_local_style:
      style_config = file_resources.GetDefaultStyleForDir(os.getcwd())

    source = [line.rstrip() for line in original_source]
    reformatted_source, _ = yapf_api.FormatCode(
        py3compat.unicode('\n'.join(source) + '\n'),
        filename='<stdin>',
        style_config=style_config,
        lines=lines,
        verify=args.verify)
    file_resources.WriteReformattedCode('<stdout>', reformatted_source)
    return 0

  files = file_resources.GetCommandLineFiles(args.files, args.recursive,
                                             args.exclude)
  if not files:
    raise errors.YapfError('Input filenames did not match any python files')

  changed = FormatFiles(
      files,
      lines,
      style_config=args.style,
      no_local_style=args.no_local_style,
      in_place=args.in_place,
      print_diff=args.diff,
      verify=args.verify,
      parallel=args.parallel,
      verbose=args.verbose)
  return 1 if changed and args.diff else 0


def FormatFiles(filenames,
                lines,
                style_config=None,
                no_local_style=False,
                in_place=False,
                print_diff=False,
                verify=False,
                parallel=False,
                verbose=False):
  """Format a list of files.

  Arguments:
    filenames: (list of unicode) A list of files to reformat.
    lines: (list of tuples of integers) A list of tuples of lines, [start, end],
      that we want to format. The lines are 1-based indexed. This argument
      overrides the 'args.lines'. It can be used by third-party code (e.g.,
      IDEs) when reformatting a snippet of code.
    style_config: (string) Style name or file path.
    no_local_style: (string) If style_config is None don't search for
      directory-local style configuration.
    in_place: (bool) Modify the files in place.
    print_diff: (bool) Instead of returning the reformatted source, return a
      diff that turns the formatted source into reformatter source.
    verify: (bool) True if reformatted code should be verified for syntax.
    parallel: (bool) True if should format multiple files in parallel.
    verbose: (bool) True if should print out filenames while processing.

  Returns:
    True if the source code changed in any of the files being formatted.
  """
  changed = False
  if parallel:
    import multiprocessing  # pylint: disable=g-import-not-at-top
    import concurrent.futures  # pylint: disable=g-import-not-at-top
    workers = min(multiprocessing.cpu_count(), len(filenames))
    with concurrent.futures.ProcessPoolExecutor(workers) as executor:
      future_formats = [
          executor.submit(_FormatFile, filename, lines, style_config,
                          no_local_style, in_place, print_diff, verify, verbose)
          for filename in filenames
      ]
      for future in concurrent.futures.as_completed(future_formats):
        changed |= future.result()
  else:
    for filename in filenames:
      changed |= _FormatFile(filename, lines, style_config, no_local_style,
                             in_place, print_diff, verify, verbose)
  return changed


def _FormatFile(filename,
                lines,
                style_config=None,
                no_local_style=False,
                in_place=False,
                print_diff=False,
                verify=False,
                verbose=False):
  if verbose:
    print('Reformatting %s' % filename)
  if style_config is None and not no_local_style:
    style_config = file_resources.GetDefaultStyleForDir(
        os.path.dirname(filename))
  try:
    reformatted_code, encoding, has_change = yapf_api.FormatFile(
        filename,
        in_place=in_place,
        style_config=style_config,
        lines=lines,
        print_diff=print_diff,
        verify=verify,
        logger=logging.warning)
    if not in_place and reformatted_code:
      file_resources.WriteReformattedCode(filename, reformatted_code, encoding,
                                          in_place)
    return has_change
  except SyntaxError as e:
    e.filename = filename
    raise


def _GetLines(line_strings):
  """Parses the start and end lines from a line string like 'start-end'.

  Arguments:
    line_strings: (array of string) A list of strings representing a line
      range like 'start-end'.

  Returns:
    A list of tuples of the start and end line numbers.

  Raises:
    ValueError: If the line string failed to parse or was an invalid line range.
  """
  lines = []
  for line_string in line_strings:
    # The 'list' here is needed by Python 3.
    line = list(map(int, line_string.split('-', 1)))
    if line[0] < 1:
      raise errors.YapfError('invalid start of line range: %r' % line)
    if line[0] > line[1]:
      raise errors.YapfError('end comes before start in line range: %r', line)
    lines.append(tuple(line))
  return lines


def run_main():  # pylint: disable=invalid-name
  try:
    sys.exit(main(sys.argv))
  except errors.YapfError as e:
    sys.stderr.write('yapf: ' + str(e) + '\n')
    sys.exit(1)


if __name__ == '__main__':
  run_main()