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