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"""Interface to file resources. 15 16This module provides functions for interfacing with files: opening, writing, and 17querying. 18""" 19 20import fnmatch 21import os 22import re 23 24from lib2to3.pgen2 import tokenize 25 26from yapf.yapflib import errors 27from yapf.yapflib import py3compat 28from yapf.yapflib import style 29 30CR = '\r' 31LF = '\n' 32CRLF = '\r\n' 33 34 35def GetDefaultStyleForDir(dirname): 36 """Return default style name for a given directory. 37 38 Looks for .style.yapf or setup.cfg in the parent directories. 39 40 Arguments: 41 dirname: (unicode) The name of the directory. 42 43 Returns: 44 The filename if found, otherwise return the global default (pep8). 45 """ 46 dirname = os.path.abspath(dirname) 47 while True: 48 # See if we have a .style.yapf file. 49 style_file = os.path.join(dirname, style.LOCAL_STYLE) 50 if os.path.exists(style_file): 51 return style_file 52 53 # See if we have a setup.cfg file with a '[yapf]' section. 54 config_file = os.path.join(dirname, style.SETUP_CONFIG) 55 if os.path.exists(config_file): 56 with open(config_file) as fd: 57 config = py3compat.ConfigParser() 58 config.read_file(fd) 59 if config.has_section('yapf'): 60 return config_file 61 62 dirname = os.path.dirname(dirname) 63 if (not dirname or not os.path.basename(dirname) or 64 dirname == os.path.abspath(os.path.sep)): 65 break 66 67 global_file = os.path.expanduser(style.GLOBAL_STYLE) 68 if os.path.exists(global_file): 69 return global_file 70 71 return style.DEFAULT_STYLE 72 73 74def GetCommandLineFiles(command_line_file_list, recursive, exclude): 75 """Return the list of files specified on the command line.""" 76 return _FindPythonFiles(command_line_file_list, recursive, exclude) 77 78 79def WriteReformattedCode(filename, 80 reformatted_code, 81 encoding='', 82 in_place=False): 83 """Emit the reformatted code. 84 85 Write the reformatted code into the file, if in_place is True. Otherwise, 86 write to stdout. 87 88 Arguments: 89 filename: (unicode) The name of the unformatted file. 90 reformatted_code: (unicode) The reformatted code. 91 encoding: (unicode) The encoding of the file. 92 in_place: (bool) If True, then write the reformatted code to the file. 93 """ 94 if in_place: 95 with py3compat.open_with_encoding( 96 filename, mode='w', encoding=encoding, newline='') as fd: 97 fd.write(reformatted_code) 98 else: 99 py3compat.EncodeAndWriteToStdout(reformatted_code) 100 101 102def LineEnding(lines): 103 """Retrieve the line ending of the original source.""" 104 endings = {CRLF: 0, CR: 0, LF: 0} 105 for line in lines: 106 if line.endswith(CRLF): 107 endings[CRLF] += 1 108 elif line.endswith(CR): 109 endings[CR] += 1 110 elif line.endswith(LF): 111 endings[LF] += 1 112 return (sorted(endings, key=endings.get, reverse=True) or [LF])[0] 113 114 115def _FindPythonFiles(filenames, recursive, exclude): 116 """Find all Python files.""" 117 if exclude and any(e.startswith('./') for e in exclude): 118 raise errors.YapfError("path in '--exclude' should not start with ./") 119 120 python_files = [] 121 for filename in filenames: 122 if filename != '.' and exclude and IsIgnored(filename, exclude): 123 continue 124 if os.path.isdir(filename): 125 if recursive: 126 # TODO(morbo): Look into a version of os.walk that can handle recursion. 127 excluded_dirs = [] 128 for dirpath, _, filelist in os.walk(filename): 129 if dirpath != '.' and exclude and IsIgnored(dirpath, exclude): 130 excluded_dirs.append(dirpath) 131 continue 132 elif any(dirpath.startswith(e) for e in excluded_dirs): 133 continue 134 for f in filelist: 135 filepath = os.path.join(dirpath, f) 136 if exclude and IsIgnored(filepath, exclude): 137 continue 138 if IsPythonFile(filepath): 139 python_files.append(filepath) 140 else: 141 raise errors.YapfError( 142 "directory specified without '--recursive' flag: %s" % filename) 143 elif os.path.isfile(filename): 144 python_files.append(filename) 145 146 return python_files 147 148 149def IsIgnored(path, exclude): 150 """Return True if filename matches any patterns in exclude.""" 151 path = path.lstrip('/') 152 while path.startswith('./'): 153 path = path[2:] 154 return any(fnmatch.fnmatch(path, e.rstrip('/')) for e in exclude) 155 156 157def IsPythonFile(filename): 158 """Return True if filename is a Python file.""" 159 if os.path.splitext(filename)[1] == '.py': 160 return True 161 162 try: 163 with open(filename, 'rb') as fd: 164 encoding = tokenize.detect_encoding(fd.readline)[0] 165 166 # Check for correctness of encoding. 167 with py3compat.open_with_encoding( 168 filename, mode='r', encoding=encoding) as fd: 169 fd.read() 170 except UnicodeDecodeError: 171 encoding = 'latin-1' 172 except (IOError, SyntaxError): 173 # If we fail to detect encoding (or the encoding cookie is incorrect - which 174 # will make detect_encoding raise SyntaxError), assume it's not a Python 175 # file. 176 return False 177 178 try: 179 with py3compat.open_with_encoding( 180 filename, mode='r', encoding=encoding) as fd: 181 first_line = fd.readline(256) 182 except IOError: 183 return False 184 185 return re.match(r'^#!.*\bpython[23]?\b', first_line) 186 187 188def FileEncoding(filename): 189 """Return the file's encoding.""" 190 with open(filename, 'rb') as fd: 191 return tokenize.detect_encoding(fd.readline)[0] 192