# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Interface to file resources. This module provides functions for interfacing with files: opening, writing, and querying. """ import fnmatch import os import re from lib2to3.pgen2 import tokenize from yapf.yapflib import errors from yapf.yapflib import py3compat from yapf.yapflib import style CR = '\r' LF = '\n' CRLF = '\r\n' def _GetExcludePatternsFromFile(filename): """Get a list of file patterns to ignore.""" ignore_patterns = [] # See if we have a .yapfignore file. if os.path.isfile(filename) and os.access(filename, os.R_OK): with open(filename, 'r') as fd: for line in fd: if line.strip() and not line.startswith('#'): ignore_patterns.append(line.strip()) if any(e.startswith('./') for e in ignore_patterns): raise errors.YapfError('path in .yapfignore should not start with ./') return ignore_patterns def GetExcludePatternsForDir(dirname): """Return patterns of files to exclude from ignorefile in a given directory. Looks for .yapfignore in the directory dirname. Arguments: dirname: (unicode) The name of the directory. Returns: A List of file patterns to exclude if ignore file is found, otherwise empty List. """ ignore_file = os.path.join(dirname, '.yapfignore') return _GetExcludePatternsFromFile(ignore_file) def GetDefaultStyleForDir(dirname, default_style=style.DEFAULT_STYLE): """Return default style name for a given directory. Looks for .style.yapf or setup.cfg in the parent directories. Arguments: dirname: (unicode) The name of the directory. default_style: The style to return if nothing is found. Defaults to the global default style ('pep8') unless otherwise specified. Returns: The filename if found, otherwise return the default style. """ dirname = os.path.abspath(dirname) while True: # See if we have a .style.yapf file. style_file = os.path.join(dirname, style.LOCAL_STYLE) if os.path.exists(style_file): return style_file # See if we have a setup.cfg file with a '[yapf]' section. config_file = os.path.join(dirname, style.SETUP_CONFIG) try: fd = open(config_file) except IOError: pass # It's okay if it's not there. else: with fd: config = py3compat.ConfigParser() config.read_file(fd) if config.has_section('yapf'): return config_file if (not dirname or not os.path.basename(dirname) or dirname == os.path.abspath(os.path.sep)): break dirname = os.path.dirname(dirname) global_file = os.path.expanduser(style.GLOBAL_STYLE) if os.path.exists(global_file): return global_file return default_style def GetCommandLineFiles(command_line_file_list, recursive, exclude): """Return the list of files specified on the command line.""" return _FindPythonFiles(command_line_file_list, recursive, exclude) def WriteReformattedCode(filename, reformatted_code, encoding='', in_place=False): """Emit the reformatted code. Write the reformatted code into the file, if in_place is True. Otherwise, write to stdout. Arguments: filename: (unicode) The name of the unformatted file. reformatted_code: (unicode) The reformatted code. encoding: (unicode) The encoding of the file. in_place: (bool) If True, then write the reformatted code to the file. """ if in_place: with py3compat.open_with_encoding( filename, mode='w', encoding=encoding, newline='') as fd: fd.write(reformatted_code) else: py3compat.EncodeAndWriteToStdout(reformatted_code) def LineEnding(lines): """Retrieve the line ending of the original source.""" endings = {CRLF: 0, CR: 0, LF: 0} for line in lines: if line.endswith(CRLF): endings[CRLF] += 1 elif line.endswith(CR): endings[CR] += 1 elif line.endswith(LF): endings[LF] += 1 return (sorted(endings, key=endings.get, reverse=True) or [LF])[0] def _FindPythonFiles(filenames, recursive, exclude): """Find all Python files.""" if exclude and any(e.startswith('./') for e in exclude): raise errors.YapfError("path in '--exclude' should not start with ./") python_files = [] for filename in filenames: if filename != '.' and exclude and IsIgnored(filename, exclude): continue if os.path.isdir(filename): if not recursive: raise errors.YapfError( "directory specified without '--recursive' flag: %s" % filename) # TODO(morbo): Look into a version of os.walk that can handle recursion. excluded_dirs = [] for dirpath, _, filelist in os.walk(filename): if dirpath != '.' and exclude and IsIgnored(dirpath, exclude): excluded_dirs.append(dirpath) continue elif any(dirpath.startswith(e) for e in excluded_dirs): continue for f in filelist: filepath = os.path.join(dirpath, f) if exclude and IsIgnored(filepath, exclude): continue if IsPythonFile(filepath): python_files.append(filepath) elif os.path.isfile(filename): python_files.append(filename) return python_files def IsIgnored(path, exclude): """Return True if filename matches any patterns in exclude.""" path = path.lstrip('/') while path.startswith('./'): path = path[2:] return any(fnmatch.fnmatch(path, e.rstrip('/')) for e in exclude) def IsPythonFile(filename): """Return True if filename is a Python file.""" if os.path.splitext(filename)[1] == '.py': return True try: with open(filename, 'rb') as fd: encoding = tokenize.detect_encoding(fd.readline)[0] # Check for correctness of encoding. with py3compat.open_with_encoding( filename, mode='r', encoding=encoding) as fd: fd.read() except UnicodeDecodeError: encoding = 'latin-1' except (IOError, SyntaxError): # If we fail to detect encoding (or the encoding cookie is incorrect - which # will make detect_encoding raise SyntaxError), assume it's not a Python # file. return False try: with py3compat.open_with_encoding( filename, mode='r', encoding=encoding) as fd: first_line = fd.readline(256) except IOError: return False return re.match(r'^#!.*\bpython[23]?\b', first_line) def FileEncoding(filename): """Return the file's encoding.""" with open(filename, 'rb') as fd: return tokenize.detect_encoding(fd.readline)[0]