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 _GetExcludePatternsFromYapfIgnore(filename): 36 """Get a list of file patterns to ignore from .yapfignore.""" 37 ignore_patterns = [] 38 if os.path.isfile(filename) and os.access(filename, os.R_OK): 39 with open(filename, 'r') as fd: 40 for line in fd: 41 if line.strip() and not line.startswith('#'): 42 ignore_patterns.append(line.strip()) 43 44 if any(e.startswith('./') for e in ignore_patterns): 45 raise errors.YapfError('path in .yapfignore should not start with ./') 46 47 return ignore_patterns 48 49 50def _GetExcludePatternsFromPyprojectToml(filename): 51 """Get a list of file patterns to ignore from pyproject.toml.""" 52 ignore_patterns = [] 53 try: 54 import toml 55 except ImportError: 56 raise errors.YapfError( 57 "toml package is needed for using pyproject.toml as a " 58 "configuration file") 59 60 if os.path.isfile(filename) and os.access(filename, os.R_OK): 61 pyproject_toml = toml.load(filename) 62 ignore_patterns = pyproject_toml.get('tool', 63 {}).get('yapfignore', 64 {}).get('ignore_patterns', []) 65 if any(e.startswith('./') for e in ignore_patterns): 66 raise errors.YapfError('path in pyproject.toml should not start with ./') 67 68 return ignore_patterns 69 70 71def GetExcludePatternsForDir(dirname): 72 """Return patterns of files to exclude from ignorefile in a given directory. 73 74 Looks for .yapfignore in the directory dirname. 75 76 Arguments: 77 dirname: (unicode) The name of the directory. 78 79 Returns: 80 A List of file patterns to exclude if ignore file is found, otherwise empty 81 List. 82 """ 83 ignore_patterns = [] 84 85 yapfignore_file = os.path.join(dirname, '.yapfignore') 86 if os.path.exists(yapfignore_file): 87 ignore_patterns += _GetExcludePatternsFromYapfIgnore(yapfignore_file) 88 89 pyproject_toml_file = os.path.join(dirname, 'pyproject.toml') 90 if os.path.exists(pyproject_toml_file): 91 ignore_patterns += _GetExcludePatternsFromPyprojectToml(pyproject_toml_file) 92 return ignore_patterns 93 94 95def GetDefaultStyleForDir(dirname, default_style=style.DEFAULT_STYLE): 96 """Return default style name for a given directory. 97 98 Looks for .style.yapf or setup.cfg or pyproject.toml in the parent 99 directories. 100 101 Arguments: 102 dirname: (unicode) The name of the directory. 103 default_style: The style to return if nothing is found. Defaults to the 104 global default style ('pep8') unless otherwise specified. 105 106 Returns: 107 The filename if found, otherwise return the default style. 108 """ 109 dirname = os.path.abspath(dirname) 110 while True: 111 # See if we have a .style.yapf file. 112 style_file = os.path.join(dirname, style.LOCAL_STYLE) 113 if os.path.exists(style_file): 114 return style_file 115 116 # See if we have a setup.cfg file with a '[yapf]' section. 117 config_file = os.path.join(dirname, style.SETUP_CONFIG) 118 try: 119 fd = open(config_file) 120 except IOError: 121 pass # It's okay if it's not there. 122 else: 123 with fd: 124 config = py3compat.ConfigParser() 125 config.read_file(fd) 126 if config.has_section('yapf'): 127 return config_file 128 129 # See if we have a pyproject.toml file with a '[tool.yapf]' section. 130 config_file = os.path.join(dirname, style.PYPROJECT_TOML) 131 try: 132 fd = open(config_file) 133 except IOError: 134 pass # It's okay if it's not there. 135 else: 136 with fd: 137 try: 138 import toml 139 except ImportError: 140 raise errors.YapfError( 141 "toml package is needed for using pyproject.toml as a " 142 "configuration file") 143 144 pyproject_toml = toml.load(config_file) 145 style_dict = pyproject_toml.get('tool', {}).get('yapf', None) 146 if style_dict is not None: 147 return config_file 148 149 if (not dirname or not os.path.basename(dirname) or 150 dirname == os.path.abspath(os.path.sep)): 151 break 152 dirname = os.path.dirname(dirname) 153 154 global_file = os.path.expanduser(style.GLOBAL_STYLE) 155 if os.path.exists(global_file): 156 return global_file 157 158 return default_style 159 160 161def GetCommandLineFiles(command_line_file_list, recursive, exclude): 162 """Return the list of files specified on the command line.""" 163 return _FindPythonFiles(command_line_file_list, recursive, exclude) 164 165 166def WriteReformattedCode(filename, 167 reformatted_code, 168 encoding='', 169 in_place=False): 170 """Emit the reformatted code. 171 172 Write the reformatted code into the file, if in_place is True. Otherwise, 173 write to stdout. 174 175 Arguments: 176 filename: (unicode) The name of the unformatted file. 177 reformatted_code: (unicode) The reformatted code. 178 encoding: (unicode) The encoding of the file. 179 in_place: (bool) If True, then write the reformatted code to the file. 180 """ 181 if in_place: 182 with py3compat.open_with_encoding( 183 filename, mode='w', encoding=encoding, newline='') as fd: 184 fd.write(reformatted_code) 185 else: 186 py3compat.EncodeAndWriteToStdout(reformatted_code) 187 188 189def LineEnding(lines): 190 """Retrieve the line ending of the original source.""" 191 endings = {CRLF: 0, CR: 0, LF: 0} 192 for line in lines: 193 if line.endswith(CRLF): 194 endings[CRLF] += 1 195 elif line.endswith(CR): 196 endings[CR] += 1 197 elif line.endswith(LF): 198 endings[LF] += 1 199 return (sorted(endings, key=endings.get, reverse=True) or [LF])[0] 200 201 202def _FindPythonFiles(filenames, recursive, exclude): 203 """Find all Python files.""" 204 if exclude and any(e.startswith('./') for e in exclude): 205 raise errors.YapfError("path in '--exclude' should not start with ./") 206 exclude = exclude and [e.rstrip("/" + os.path.sep) for e in exclude] 207 208 python_files = [] 209 for filename in filenames: 210 if filename != '.' and exclude and IsIgnored(filename, exclude): 211 continue 212 if os.path.isdir(filename): 213 if not recursive: 214 raise errors.YapfError( 215 "directory specified without '--recursive' flag: %s" % filename) 216 217 # TODO(morbo): Look into a version of os.walk that can handle recursion. 218 excluded_dirs = [] 219 for dirpath, dirnames, filelist in os.walk(filename): 220 if dirpath != '.' and exclude and IsIgnored(dirpath, exclude): 221 excluded_dirs.append(dirpath) 222 continue 223 elif any(dirpath.startswith(e) for e in excluded_dirs): 224 continue 225 for f in filelist: 226 filepath = os.path.join(dirpath, f) 227 if exclude and IsIgnored(filepath, exclude): 228 continue 229 if IsPythonFile(filepath): 230 python_files.append(filepath) 231 # To prevent it from scanning the contents excluded folders, os.walk() 232 # lets you amend its list of child dirs `dirnames`. These edits must be 233 # made in-place instead of creating a modified copy of `dirnames`. 234 # list.remove() is slow and list.pop() is a headache. Instead clear 235 # `dirnames` then repopulate it. 236 dirnames_ = [dirnames.pop(0) for i in range(len(dirnames))] 237 for dirname in dirnames_: 238 dir_ = os.path.join(dirpath, dirname) 239 if IsIgnored(dir_, exclude): 240 excluded_dirs.append(dir_) 241 else: 242 dirnames.append(dirname) 243 244 elif os.path.isfile(filename): 245 python_files.append(filename) 246 247 return python_files 248 249 250def IsIgnored(path, exclude): 251 """Return True if filename matches any patterns in exclude.""" 252 if exclude is None: 253 return False 254 path = path.lstrip(os.path.sep) 255 while path.startswith('.' + os.path.sep): 256 path = path[2:] 257 return any(fnmatch.fnmatch(path, e.rstrip(os.path.sep)) for e in exclude) 258 259 260def IsPythonFile(filename): 261 """Return True if filename is a Python file.""" 262 if os.path.splitext(filename)[1] == '.py': 263 return True 264 265 try: 266 with open(filename, 'rb') as fd: 267 encoding = tokenize.detect_encoding(fd.readline)[0] 268 269 # Check for correctness of encoding. 270 with py3compat.open_with_encoding( 271 filename, mode='r', encoding=encoding) as fd: 272 fd.read() 273 except UnicodeDecodeError: 274 encoding = 'latin-1' 275 except (IOError, SyntaxError): 276 # If we fail to detect encoding (or the encoding cookie is incorrect - which 277 # will make detect_encoding raise SyntaxError), assume it's not a Python 278 # file. 279 return False 280 281 try: 282 with py3compat.open_with_encoding( 283 filename, mode='r', encoding=encoding) as fd: 284 first_line = fd.readline(256) 285 except IOError: 286 return False 287 288 return re.match(r'^#!.*\bpython[23]?\b', first_line) 289 290 291def FileEncoding(filename): 292 """Return the file's encoding.""" 293 with open(filename, 'rb') as fd: 294 return tokenize.detect_encoding(fd.readline)[0] 295