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