file_resources.py 9.4 KB

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