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