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