• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""Entry points for YAPF.
15
16The main APIs that YAPF exposes to drive the reformatting.
17
18  FormatFile(): reformat a file.
19  FormatCode(): reformat a string of code.
20
21These APIs have some common arguments:
22
23  style_config: (string) Either a style name or a path to a file that contains
24    formatting style settings. If None is specified, use the default style
25    as set in style.DEFAULT_STYLE_FACTORY
26  lines: (list of tuples of integers) A list of tuples of lines, [start, end],
27    that we want to format. The lines are 1-based indexed. It can be used by
28    third-party code (e.g., IDEs) when reformatting a snippet of code rather
29    than a whole file.
30  print_diff: (bool) Instead of returning the reformatted source, return a
31    diff that turns the formatted source into reformatter source.
32  verify: (bool) True if reformatted code should be verified for syntax.
33"""
34
35import difflib
36import re
37import sys
38
39from lib2to3.pgen2 import parse
40from lib2to3.pgen2 import tokenize
41
42from yapf.yapflib import blank_line_calculator
43from yapf.yapflib import comment_splicer
44from yapf.yapflib import continuation_splicer
45from yapf.yapflib import errors
46from yapf.yapflib import file_resources
47from yapf.yapflib import identify_container
48from yapf.yapflib import py3compat
49from yapf.yapflib import pytree_unwrapper
50from yapf.yapflib import pytree_utils
51from yapf.yapflib import reformatter
52from yapf.yapflib import split_penalty
53from yapf.yapflib import style
54from yapf.yapflib import subtype_assigner
55
56
57def FormatFile(filename,
58               style_config=None,
59               lines=None,
60               print_diff=False,
61               verify=False,
62               in_place=False,
63               logger=None):
64  """Format a single Python file and return the formatted code.
65
66  Arguments:
67    filename: (unicode) The file to reformat.
68    style_config: (string) Either a style name or a path to a file that contains
69      formatting style settings. If None is specified, use the default style
70      as set in style.DEFAULT_STYLE_FACTORY
71    lines: (list of tuples of integers) A list of tuples of lines, [start, end],
72      that we want to format. The lines are 1-based indexed. It can be used by
73      third-party code (e.g., IDEs) when reformatting a snippet of code rather
74      than a whole file.
75    print_diff: (bool) Instead of returning the reformatted source, return a
76      diff that turns the formatted source into reformatter source.
77    verify: (bool) True if reformatted code should be verified for syntax.
78    in_place: (bool) If True, write the reformatted code back to the file.
79    logger: (io streamer) A stream to output logging.
80
81  Returns:
82    Tuple of (reformatted_code, encoding, changed). reformatted_code is None if
83    the file is successfully written to (having used in_place). reformatted_code
84    is a diff if print_diff is True.
85
86  Raises:
87    IOError: raised if there was an error reading the file.
88    ValueError: raised if in_place and print_diff are both specified.
89  """
90  _CheckPythonVersion()
91
92  if in_place and print_diff:
93    raise ValueError('Cannot pass both in_place and print_diff.')
94
95  original_source, newline, encoding = ReadFile(filename, logger)
96  reformatted_source, changed = FormatCode(
97      original_source,
98      style_config=style_config,
99      filename=filename,
100      lines=lines,
101      print_diff=print_diff,
102      verify=verify)
103  if reformatted_source.rstrip('\n'):
104    lines = reformatted_source.rstrip('\n').split('\n')
105    reformatted_source = newline.join(iter(lines)) + newline
106  if in_place:
107    if original_source and original_source != reformatted_source:
108      file_resources.WriteReformattedCode(filename, reformatted_source,
109                                          encoding, in_place)
110    return None, encoding, changed
111
112  return reformatted_source, encoding, changed
113
114
115def FormatTree(tree, style_config=None, lines=None, verify=False):
116  """Format a parsed lib2to3 pytree.
117
118  This provides an alternative entry point to YAPF.
119
120  Arguments:
121    tree: (pytree.Node) The root of the pytree to format.
122    style_config: (string) Either a style name or a path to a file that contains
123      formatting style settings. If None is specified, use the default style
124      as set in style.DEFAULT_STYLE_FACTORY
125    lines: (list of tuples of integers) A list of tuples of lines, [start, end],
126      that we want to format. The lines are 1-based indexed. It can be used by
127      third-party code (e.g., IDEs) when reformatting a snippet of code rather
128      than a whole file.
129    verify: (bool) True if reformatted code should be verified for syntax.
130
131  Returns:
132    The source formatted according to the given formatting style.
133  """
134  _CheckPythonVersion()
135  style.SetGlobalStyle(style.CreateStyleFromConfig(style_config))
136
137  # Run passes on the tree, modifying it in place.
138  comment_splicer.SpliceComments(tree)
139  continuation_splicer.SpliceContinuations(tree)
140  subtype_assigner.AssignSubtypes(tree)
141  identify_container.IdentifyContainers(tree)
142  split_penalty.ComputeSplitPenalties(tree)
143  blank_line_calculator.CalculateBlankLines(tree)
144
145  llines = pytree_unwrapper.UnwrapPyTree(tree)
146  for lline in llines:
147    lline.CalculateFormattingInformation()
148
149  lines = _LineRangesToSet(lines)
150  _MarkLinesToFormat(llines, lines)
151  return reformatter.Reformat(_SplitSemicolons(llines), verify, lines)
152
153
154def FormatCode(unformatted_source,
155               filename='<unknown>',
156               style_config=None,
157               lines=None,
158               print_diff=False,
159               verify=False):
160  """Format a string of Python code.
161
162  This provides an alternative entry point to YAPF.
163
164  Arguments:
165    unformatted_source: (unicode) The code to format.
166    filename: (unicode) The name of the file being reformatted.
167    style_config: (string) Either a style name or a path to a file that contains
168      formatting style settings. If None is specified, use the default style
169      as set in style.DEFAULT_STYLE_FACTORY
170    lines: (list of tuples of integers) A list of tuples of lines, [start, end],
171      that we want to format. The lines are 1-based indexed. It can be used by
172      third-party code (e.g., IDEs) when reformatting a snippet of code rather
173      than a whole file.
174    print_diff: (bool) Instead of returning the reformatted source, return a
175      diff that turns the formatted source into reformatter source.
176    verify: (bool) True if reformatted code should be verified for syntax.
177
178  Returns:
179    Tuple of (reformatted_source, changed). reformatted_source conforms to the
180    desired formatting style. changed is True if the source changed.
181  """
182  try:
183    tree = pytree_utils.ParseCodeToTree(unformatted_source)
184  except Exception as e:
185    e.filename = filename
186    raise errors.YapfError(errors.FormatErrorMsg(e))
187
188  reformatted_source = FormatTree(
189      tree, style_config=style_config, lines=lines, verify=verify)
190
191  if unformatted_source == reformatted_source:
192    return '' if print_diff else reformatted_source, False
193
194  code_diff = _GetUnifiedDiff(
195      unformatted_source, reformatted_source, filename=filename)
196
197  if print_diff:
198    return code_diff, code_diff.strip() != ''  # pylint: disable=g-explicit-bool-comparison # noqa
199
200  return reformatted_source, True
201
202
203def _CheckPythonVersion():  # pragma: no cover
204  errmsg = 'yapf is only supported for Python 2.7 or 3.4+'
205  if sys.version_info[0] == 2:
206    if sys.version_info[1] < 7:
207      raise RuntimeError(errmsg)
208  elif sys.version_info[0] == 3:
209    if sys.version_info[1] < 4:
210      raise RuntimeError(errmsg)
211
212
213def ReadFile(filename, logger=None):
214  """Read the contents of the file.
215
216  An optional logger can be specified to emit messages to your favorite logging
217  stream. If specified, then no exception is raised. This is external so that it
218  can be used by third-party applications.
219
220  Arguments:
221    filename: (unicode) The name of the file.
222    logger: (function) A function or lambda that takes a string and emits it.
223
224  Returns:
225    The contents of filename.
226
227  Raises:
228    IOError: raised if there was an error reading the file.
229  """
230  try:
231    encoding = file_resources.FileEncoding(filename)
232
233    # Preserves line endings.
234    with py3compat.open_with_encoding(
235        filename, mode='r', encoding=encoding, newline='') as fd:
236      lines = fd.readlines()
237
238    line_ending = file_resources.LineEnding(lines)
239    source = '\n'.join(line.rstrip('\r\n') for line in lines) + '\n'
240    return source, line_ending, encoding
241  except IOError as e:  # pragma: no cover
242    if logger:
243      logger(e)
244    e.args = (e.args[0], (filename, e.args[1][1], e.args[1][2], e.args[1][3]))
245    raise
246  except UnicodeDecodeError as e:  # pragma: no cover
247    if logger:
248      logger('Could not parse %s! Consider excluding this file with --exclude.',
249             filename)
250      logger(e)
251    e.args = (e.args[0], (filename, e.args[1][1], e.args[1][2], e.args[1][3]))
252    raise
253
254
255def _SplitSemicolons(lines):
256  res = []
257  for line in lines:
258    res.extend(line.Split())
259  return res
260
261
262DISABLE_PATTERN = r'^#.*\byapf:\s*disable\b'
263ENABLE_PATTERN = r'^#.*\byapf:\s*enable\b'
264
265
266def _LineRangesToSet(line_ranges):
267  """Return a set of lines in the range."""
268
269  if line_ranges is None:
270    return None
271
272  line_set = set()
273  for low, high in sorted(line_ranges):
274    line_set.update(range(low, high + 1))
275
276  return line_set
277
278
279def _MarkLinesToFormat(llines, lines):
280  """Skip sections of code that we shouldn't reformat."""
281  if lines:
282    for uwline in llines:
283      uwline.disable = not lines.intersection(
284          range(uwline.lineno, uwline.last.lineno + 1))
285
286  # Now go through the lines and disable any lines explicitly marked as
287  # disabled.
288  index = 0
289  while index < len(llines):
290    uwline = llines[index]
291    if uwline.is_comment:
292      if _DisableYAPF(uwline.first.value.strip()):
293        index += 1
294        while index < len(llines):
295          uwline = llines[index]
296          line = uwline.first.value.strip()
297          if uwline.is_comment and _EnableYAPF(line):
298            if not _DisableYAPF(line):
299              break
300          uwline.disable = True
301          index += 1
302    elif re.search(DISABLE_PATTERN, uwline.last.value.strip(), re.IGNORECASE):
303      uwline.disable = True
304    index += 1
305
306
307def _DisableYAPF(line):
308  return (re.search(DISABLE_PATTERN,
309                    line.split('\n')[0].strip(), re.IGNORECASE) or
310          re.search(DISABLE_PATTERN,
311                    line.split('\n')[-1].strip(), re.IGNORECASE))
312
313
314def _EnableYAPF(line):
315  return (re.search(ENABLE_PATTERN,
316                    line.split('\n')[0].strip(), re.IGNORECASE) or
317          re.search(ENABLE_PATTERN,
318                    line.split('\n')[-1].strip(), re.IGNORECASE))
319
320
321def _GetUnifiedDiff(before, after, filename='code'):
322  """Get a unified diff of the changes.
323
324  Arguments:
325    before: (unicode) The original source code.
326    after: (unicode) The reformatted source code.
327    filename: (unicode) The code's filename.
328
329  Returns:
330    The unified diff text.
331  """
332  before = before.splitlines()
333  after = after.splitlines()
334  return '\n'.join(
335      difflib.unified_diff(
336          before,
337          after,
338          filename,
339          filename,
340          '(original)',
341          '(reformatted)',
342          lineterm='')) + '\n'
343