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