• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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