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
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 lib2to3.pgen2 import tokenize
36
37from yapf.yapflib import errors
38from yapf.yapflib import file_resources
39from yapf.yapflib import py3compat
40from yapf.yapflib import style
41from yapf.yapflib import yapf_api
42
43__version__ = '0.31.0'
44
45
46def main(argv):
47  """Main program.
48
49  Arguments:
50    argv: command-line arguments, such as sys.argv (including the program name
51      in argv[0]).
52
53  Returns:
54    Zero on successful program termination, non-zero otherwise.
55    With --diff: zero if there were no changes, non-zero otherwise.
56
57  Raises:
58    YapfError: if none of the supplied files were Python files.
59  """
60  parser = _BuildParser()
61  args = parser.parse_args(argv[1:])
62  if args.version:
63    print('yapf {}'.format(__version__))
64    return 0
65
66  style_config = args.style
67
68  if args.style_help:
69    _PrintHelp(args)
70    return 0
71
72  if args.lines and len(args.files) > 1:
73    parser.error('cannot use -l/--lines with more than one file')
74
75  lines = _GetLines(args.lines) if args.lines is not None else None
76  if not args.files:
77    # No arguments specified. Read code from stdin.
78    if args.in_place or args.diff:
79      parser.error('cannot use --in-place or --diff flags when reading '
80                   'from stdin')
81
82    original_source = []
83    while True:
84      if sys.stdin.closed:
85        break
86      try:
87        # Use 'raw_input' instead of 'sys.stdin.read', because otherwise the
88        # user will need to hit 'Ctrl-D' more than once if they're inputting
89        # the program by hand. 'raw_input' throws an EOFError exception if
90        # 'Ctrl-D' is pressed, which makes it easy to bail out of this loop.
91        original_source.append(py3compat.raw_input())
92      except EOFError:
93        break
94      except KeyboardInterrupt:
95        return 1
96
97    if style_config is None and not args.no_local_style:
98      style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
99
100    source = [line.rstrip() for line in original_source]
101    source[0] = py3compat.removeBOM(source[0])
102
103    try:
104      reformatted_source, _ = yapf_api.FormatCode(
105          py3compat.unicode('\n'.join(source) + '\n'),
106          filename='<stdin>',
107          style_config=style_config,
108          lines=lines,
109          verify=args.verify)
110    except tokenize.TokenError as e:
111      raise errors.YapfError('%s:%s' % (e.args[1][0], e.args[0]))
112
113    file_resources.WriteReformattedCode('<stdout>', reformatted_source)
114    return 0
115
116  # Get additional exclude patterns from ignorefile
117  exclude_patterns_from_ignore_file = file_resources.GetExcludePatternsForDir(
118      os.getcwd())
119
120  files = file_resources.GetCommandLineFiles(args.files, args.recursive,
121                                             (args.exclude or []) +
122                                             exclude_patterns_from_ignore_file)
123  if not files:
124    raise errors.YapfError('Input filenames did not match any python files')
125
126  changed = FormatFiles(
127      files,
128      lines,
129      style_config=args.style,
130      no_local_style=args.no_local_style,
131      in_place=args.in_place,
132      print_diff=args.diff,
133      verify=args.verify,
134      parallel=args.parallel,
135      quiet=args.quiet,
136      verbose=args.verbose)
137  return 1 if changed and (args.diff or args.quiet) else 0
138
139
140def _PrintHelp(args):
141  """Prints the help menu."""
142
143  if args.style is None and not args.no_local_style:
144    args.style = file_resources.GetDefaultStyleForDir(os.getcwd())
145  style.SetGlobalStyle(style.CreateStyleFromConfig(args.style))
146  print('[style]')
147  for option, docstring in sorted(style.Help().items()):
148    for line in docstring.splitlines():
149      print('#', line and ' ' or '', line, sep='')
150    option_value = style.Get(option)
151    if isinstance(option_value, set) or isinstance(option_value, list):
152      option_value = ', '.join(map(str, option_value))
153    print(option.lower(), '=', option_value, sep='')
154    print()
155
156
157def FormatFiles(filenames,
158                lines,
159                style_config=None,
160                no_local_style=False,
161                in_place=False,
162                print_diff=False,
163                verify=False,
164                parallel=False,
165                quiet=False,
166                verbose=False):
167  """Format a list of files.
168
169  Arguments:
170    filenames: (list of unicode) A list of files to reformat.
171    lines: (list of tuples of integers) A list of tuples of lines, [start, end],
172      that we want to format. The lines are 1-based indexed. This argument
173      overrides the 'args.lines'. It can be used by third-party code (e.g.,
174      IDEs) when reformatting a snippet of code.
175    style_config: (string) Style name or file path.
176    no_local_style: (string) If style_config is None don't search for
177      directory-local style configuration.
178    in_place: (bool) Modify the files in place.
179    print_diff: (bool) Instead of returning the reformatted source, return a
180      diff that turns the formatted source into reformatter source.
181    verify: (bool) True if reformatted code should be verified for syntax.
182    parallel: (bool) True if should format multiple files in parallel.
183    quiet: (bool) True if should output nothing.
184    verbose: (bool) True if should print out filenames while processing.
185
186  Returns:
187    True if the source code changed in any of the files being formatted.
188  """
189  changed = False
190  if parallel:
191    import multiprocessing  # pylint: disable=g-import-not-at-top
192    import concurrent.futures  # pylint: disable=g-import-not-at-top
193    workers = min(multiprocessing.cpu_count(), len(filenames))
194    with concurrent.futures.ProcessPoolExecutor(workers) as executor:
195      future_formats = [
196          executor.submit(_FormatFile, filename, lines, style_config,
197                          no_local_style, in_place, print_diff, verify, quiet,
198                          verbose) for filename in filenames
199      ]
200      for future in concurrent.futures.as_completed(future_formats):
201        changed |= future.result()
202  else:
203    for filename in filenames:
204      changed |= _FormatFile(filename, lines, style_config, no_local_style,
205                             in_place, print_diff, verify, quiet, verbose)
206  return changed
207
208
209def _FormatFile(filename,
210                lines,
211                style_config=None,
212                no_local_style=False,
213                in_place=False,
214                print_diff=False,
215                verify=False,
216                quiet=False,
217                verbose=False):
218  """Format an individual file."""
219  if verbose and not quiet:
220    print('Reformatting %s' % filename)
221
222  if style_config is None and not no_local_style:
223    style_config = file_resources.GetDefaultStyleForDir(
224        os.path.dirname(filename))
225
226  try:
227    reformatted_code, encoding, has_change = yapf_api.FormatFile(
228        filename,
229        in_place=in_place,
230        style_config=style_config,
231        lines=lines,
232        print_diff=print_diff,
233        verify=verify,
234        logger=logging.warning)
235  except tokenize.TokenError as e:
236    raise errors.YapfError('%s:%s:%s' % (filename, e.args[1][0], e.args[0]))
237  except SyntaxError as e:
238    e.filename = filename
239    raise
240
241  if not in_place and not quiet and reformatted_code:
242    file_resources.WriteReformattedCode(filename, reformatted_code, encoding,
243                                        in_place)
244  return has_change
245
246
247def _GetLines(line_strings):
248  """Parses the start and end lines from a line string like 'start-end'.
249
250  Arguments:
251    line_strings: (array of string) A list of strings representing a line
252      range like 'start-end'.
253
254  Returns:
255    A list of tuples of the start and end line numbers.
256
257  Raises:
258    ValueError: If the line string failed to parse or was an invalid line range.
259  """
260  lines = []
261  for line_string in line_strings:
262    # The 'list' here is needed by Python 3.
263    line = list(map(int, line_string.split('-', 1)))
264    if line[0] < 1:
265      raise errors.YapfError('invalid start of line range: %r' % line)
266    if line[0] > line[1]:
267      raise errors.YapfError('end comes before start in line range: %r' % line)
268    lines.append(tuple(line))
269  return lines
270
271
272def _BuildParser():
273  """Constructs the parser for the command line arguments.
274
275  Returns:
276    An ArgumentParser instance for the CLI.
277  """
278  parser = argparse.ArgumentParser(description='Formatter for Python code.')
279  parser.add_argument(
280      '-v',
281      '--version',
282      action='store_true',
283      help='show version number and exit')
284
285  diff_inplace_quiet_group = parser.add_mutually_exclusive_group()
286  diff_inplace_quiet_group.add_argument(
287      '-d',
288      '--diff',
289      action='store_true',
290      help='print the diff for the fixed source')
291  diff_inplace_quiet_group.add_argument(
292      '-i',
293      '--in-place',
294      action='store_true',
295      help='make changes to files in place')
296  diff_inplace_quiet_group.add_argument(
297      '-q',
298      '--quiet',
299      action='store_true',
300      help='output nothing and set return value')
301
302  lines_recursive_group = parser.add_mutually_exclusive_group()
303  lines_recursive_group.add_argument(
304      '-r',
305      '--recursive',
306      action='store_true',
307      help='run recursively over directories')
308  lines_recursive_group.add_argument(
309      '-l',
310      '--lines',
311      metavar='START-END',
312      action='append',
313      default=None,
314      help='range of lines to reformat, one-based')
315
316  parser.add_argument(
317      '-e',
318      '--exclude',
319      metavar='PATTERN',
320      action='append',
321      default=None,
322      help='patterns for files to exclude from formatting')
323  parser.add_argument(
324      '--style',
325      action='store',
326      help=('specify formatting style: either a style name (for example "pep8" '
327            'or "google"), or the name of a file with style settings. The '
328            'default is pep8 unless a %s or %s or %s file located in the same '
329            'directory as the source or one of its parent directories '
330            '(for stdin, the current directory is used).' %
331            (style.LOCAL_STYLE, style.SETUP_CONFIG, style.PYPROJECT_TOML)))
332  parser.add_argument(
333      '--style-help',
334      action='store_true',
335      help=('show style settings and exit; this output can be '
336            'saved to .style.yapf to make your settings '
337            'permanent'))
338  parser.add_argument(
339      '--no-local-style',
340      action='store_true',
341      help="don't search for local style definition")
342  parser.add_argument('--verify', action='store_true', help=argparse.SUPPRESS)
343  parser.add_argument(
344      '-p',
345      '--parallel',
346      action='store_true',
347      help=('run yapf in parallel when formatting multiple files. Requires '
348            'concurrent.futures in Python 2.X'))
349  parser.add_argument(
350      '-vv',
351      '--verbose',
352      action='store_true',
353      help='print out file names while processing')
354
355  parser.add_argument(
356      'files', nargs='*', help='reads from stdin when no files are specified.')
357  return parser
358
359
360def run_main():  # pylint: disable=invalid-name
361  try:
362    sys.exit(main(sys.argv))
363  except errors.YapfError as e:
364    sys.stderr.write('yapf: ' + str(e) + '\n')
365    sys.exit(1)
366
367
368if __name__ == '__main__':
369  run_main()
370