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