]> crepu.dev Git - config.git/blob - djavu-asus/emacs/elpy/rpc-venv/lib/python3.11/site-packages/black/files.py
Reorganización de directorios
[config.git] / djavu-asus / emacs / elpy / rpc-venv / lib / python3.11 / site-packages / black / files.py
1 import io
2 import os
3 import sys
4 from functools import lru_cache
5 from pathlib import Path
6 from typing import (
7 TYPE_CHECKING,
8 Any,
9 Dict,
10 Iterable,
11 Iterator,
12 List,
13 Optional,
14 Pattern,
15 Sequence,
16 Tuple,
17 Union,
18 )
19
20 from mypy_extensions import mypyc_attr
21 from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet
22 from packaging.version import InvalidVersion, Version
23 from pathspec import PathSpec
24 from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
25
26 if sys.version_info >= (3, 11):
27 try:
28 import tomllib
29 except ImportError:
30 # Help users on older alphas
31 if not TYPE_CHECKING:
32 import tomli as tomllib
33 else:
34 import tomli as tomllib
35
36 from black.handle_ipynb_magics import jupyter_dependencies_are_installed
37 from black.mode import TargetVersion
38 from black.output import err
39 from black.report import Report
40
41 if TYPE_CHECKING:
42 import colorama # noqa: F401
43
44
45 @lru_cache
46 def find_project_root(
47 srcs: Sequence[str], stdin_filename: Optional[str] = None
48 ) -> Tuple[Path, str]:
49 """Return a directory containing .git, .hg, or pyproject.toml.
50
51 That directory will be a common parent of all files and directories
52 passed in `srcs`.
53
54 If no directory in the tree contains a marker that would specify it's the
55 project root, the root of the file system is returned.
56
57 Returns a two-tuple with the first element as the project root path and
58 the second element as a string describing the method by which the
59 project root was discovered.
60 """
61 if stdin_filename is not None:
62 srcs = tuple(stdin_filename if s == "-" else s for s in srcs)
63 if not srcs:
64 srcs = [str(Path.cwd().resolve())]
65
66 path_srcs = [Path(Path.cwd(), src).resolve() for src in srcs]
67
68 # A list of lists of parents for each 'src'. 'src' is included as a
69 # "parent" of itself if it is a directory
70 src_parents = [
71 list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs
72 ]
73
74 common_base = max(
75 set.intersection(*(set(parents) for parents in src_parents)),
76 key=lambda path: path.parts,
77 )
78
79 for directory in (common_base, *common_base.parents):
80 if (directory / ".git").exists():
81 return directory, ".git directory"
82
83 if (directory / ".hg").is_dir():
84 return directory, ".hg directory"
85
86 if (directory / "pyproject.toml").is_file():
87 return directory, "pyproject.toml"
88
89 return directory, "file system root"
90
91
92 def find_pyproject_toml(
93 path_search_start: Tuple[str, ...], stdin_filename: Optional[str] = None
94 ) -> Optional[str]:
95 """Find the absolute filepath to a pyproject.toml if it exists"""
96 path_project_root, _ = find_project_root(path_search_start, stdin_filename)
97 path_pyproject_toml = path_project_root / "pyproject.toml"
98 if path_pyproject_toml.is_file():
99 return str(path_pyproject_toml)
100
101 try:
102 path_user_pyproject_toml = find_user_pyproject_toml()
103 return (
104 str(path_user_pyproject_toml)
105 if path_user_pyproject_toml.is_file()
106 else None
107 )
108 except (PermissionError, RuntimeError) as e:
109 # We do not have access to the user-level config directory, so ignore it.
110 err(f"Ignoring user configuration directory due to {e!r}")
111 return None
112
113
114 @mypyc_attr(patchable=True)
115 def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
116 """Parse a pyproject toml file, pulling out relevant parts for Black.
117
118 If parsing fails, will raise a tomllib.TOMLDecodeError.
119 """
120 with open(path_config, "rb") as f:
121 pyproject_toml = tomllib.load(f)
122 config: Dict[str, Any] = pyproject_toml.get("tool", {}).get("black", {})
123 config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
124
125 if "target_version" not in config:
126 inferred_target_version = infer_target_version(pyproject_toml)
127 if inferred_target_version is not None:
128 config["target_version"] = [v.name.lower() for v in inferred_target_version]
129
130 return config
131
132
133 def infer_target_version(
134 pyproject_toml: Dict[str, Any]
135 ) -> Optional[List[TargetVersion]]:
136 """Infer Black's target version from the project metadata in pyproject.toml.
137
138 Supports the PyPA standard format (PEP 621):
139 https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#requires-python
140
141 If the target version cannot be inferred, returns None.
142 """
143 project_metadata = pyproject_toml.get("project", {})
144 requires_python = project_metadata.get("requires-python", None)
145 if requires_python is not None:
146 try:
147 return parse_req_python_version(requires_python)
148 except InvalidVersion:
149 pass
150 try:
151 return parse_req_python_specifier(requires_python)
152 except (InvalidSpecifier, InvalidVersion):
153 pass
154
155 return None
156
157
158 def parse_req_python_version(requires_python: str) -> Optional[List[TargetVersion]]:
159 """Parse a version string (i.e. ``"3.7"``) to a list of TargetVersion.
160
161 If parsing fails, will raise a packaging.version.InvalidVersion error.
162 If the parsed version cannot be mapped to a valid TargetVersion, returns None.
163 """
164 version = Version(requires_python)
165 if version.release[0] != 3:
166 return None
167 try:
168 return [TargetVersion(version.release[1])]
169 except (IndexError, ValueError):
170 return None
171
172
173 def parse_req_python_specifier(requires_python: str) -> Optional[List[TargetVersion]]:
174 """Parse a specifier string (i.e. ``">=3.7,<3.10"``) to a list of TargetVersion.
175
176 If parsing fails, will raise a packaging.specifiers.InvalidSpecifier error.
177 If the parsed specifier cannot be mapped to a valid TargetVersion, returns None.
178 """
179 specifier_set = strip_specifier_set(SpecifierSet(requires_python))
180 if not specifier_set:
181 return None
182
183 target_version_map = {f"3.{v.value}": v for v in TargetVersion}
184 compatible_versions: List[str] = list(specifier_set.filter(target_version_map))
185 if compatible_versions:
186 return [target_version_map[v] for v in compatible_versions]
187 return None
188
189
190 def strip_specifier_set(specifier_set: SpecifierSet) -> SpecifierSet:
191 """Strip minor versions for some specifiers in the specifier set.
192
193 For background on version specifiers, see PEP 440:
194 https://peps.python.org/pep-0440/#version-specifiers
195 """
196 specifiers = []
197 for s in specifier_set:
198 if "*" in str(s):
199 specifiers.append(s)
200 elif s.operator in ["~=", "==", ">=", "==="]:
201 version = Version(s.version)
202 stripped = Specifier(f"{s.operator}{version.major}.{version.minor}")
203 specifiers.append(stripped)
204 elif s.operator == ">":
205 version = Version(s.version)
206 if len(version.release) > 2:
207 s = Specifier(f">={version.major}.{version.minor}")
208 specifiers.append(s)
209 else:
210 specifiers.append(s)
211
212 return SpecifierSet(",".join(str(s) for s in specifiers))
213
214
215 @lru_cache
216 def find_user_pyproject_toml() -> Path:
217 r"""Return the path to the top-level user configuration for black.
218
219 This looks for ~\.black on Windows and ~/.config/black on Linux and other
220 Unix systems.
221
222 May raise:
223 - RuntimeError: if the current user has no homedir
224 - PermissionError: if the current process cannot access the user's homedir
225 """
226 if sys.platform == "win32":
227 # Windows
228 user_config_path = Path.home() / ".black"
229 else:
230 config_root = os.environ.get("XDG_CONFIG_HOME", "~/.config")
231 user_config_path = Path(config_root).expanduser() / "black"
232 return user_config_path.resolve()
233
234
235 @lru_cache
236 def get_gitignore(root: Path) -> PathSpec:
237 """Return a PathSpec matching gitignore content if present."""
238 gitignore = root / ".gitignore"
239 lines: List[str] = []
240 if gitignore.is_file():
241 with gitignore.open(encoding="utf-8") as gf:
242 lines = gf.readlines()
243 try:
244 return PathSpec.from_lines("gitwildmatch", lines)
245 except GitWildMatchPatternError as e:
246 err(f"Could not parse {gitignore}: {e}")
247 raise
248
249
250 def normalize_path_maybe_ignore(
251 path: Path,
252 root: Path,
253 report: Optional[Report] = None,
254 ) -> Optional[str]:
255 """Normalize `path`. May return `None` if `path` was ignored.
256
257 `report` is where "path ignored" output goes.
258 """
259 try:
260 abspath = path if path.is_absolute() else Path.cwd() / path
261 normalized_path = abspath.resolve()
262 try:
263 root_relative_path = normalized_path.relative_to(root).as_posix()
264 except ValueError:
265 if report:
266 report.path_ignored(
267 path, f"is a symbolic link that points outside {root}"
268 )
269 return None
270
271 except OSError as e:
272 if report:
273 report.path_ignored(path, f"cannot be read because {e}")
274 return None
275
276 return root_relative_path
277
278
279 def _path_is_ignored(
280 root_relative_path: str,
281 root: Path,
282 gitignore_dict: Dict[Path, PathSpec],
283 report: Report,
284 ) -> bool:
285 path = root / root_relative_path
286 # Note that this logic is sensitive to the ordering of gitignore_dict. Callers must
287 # ensure that gitignore_dict is ordered from least specific to most specific.
288 for gitignore_path, pattern in gitignore_dict.items():
289 try:
290 relative_path = path.relative_to(gitignore_path).as_posix()
291 except ValueError:
292 break
293 if pattern.match_file(relative_path):
294 report.path_ignored(
295 path.relative_to(root), "matches a .gitignore file content"
296 )
297 return True
298 return False
299
300
301 def path_is_excluded(
302 normalized_path: str,
303 pattern: Optional[Pattern[str]],
304 ) -> bool:
305 match = pattern.search(normalized_path) if pattern else None
306 return bool(match and match.group(0))
307
308
309 def gen_python_files(
310 paths: Iterable[Path],
311 root: Path,
312 include: Pattern[str],
313 exclude: Pattern[str],
314 extend_exclude: Optional[Pattern[str]],
315 force_exclude: Optional[Pattern[str]],
316 report: Report,
317 gitignore_dict: Optional[Dict[Path, PathSpec]],
318 *,
319 verbose: bool,
320 quiet: bool,
321 ) -> Iterator[Path]:
322 """Generate all files under `path` whose paths are not excluded by the
323 `exclude_regex`, `extend_exclude`, or `force_exclude` regexes,
324 but are included by the `include` regex.
325
326 Symbolic links pointing outside of the `root` directory are ignored.
327
328 `report` is where output about exclusions goes.
329 """
330
331 assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
332 for child in paths:
333 root_relative_path = child.absolute().relative_to(root).as_posix()
334
335 # First ignore files matching .gitignore, if passed
336 if gitignore_dict and _path_is_ignored(
337 root_relative_path, root, gitignore_dict, report
338 ):
339 continue
340
341 # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options.
342 root_relative_path = "/" + root_relative_path
343 if child.is_dir():
344 root_relative_path += "/"
345
346 if path_is_excluded(root_relative_path, exclude):
347 report.path_ignored(child, "matches the --exclude regular expression")
348 continue
349
350 if path_is_excluded(root_relative_path, extend_exclude):
351 report.path_ignored(
352 child, "matches the --extend-exclude regular expression"
353 )
354 continue
355
356 if path_is_excluded(root_relative_path, force_exclude):
357 report.path_ignored(child, "matches the --force-exclude regular expression")
358 continue
359
360 normalized_path = normalize_path_maybe_ignore(child, root, report)
361 if normalized_path is None:
362 continue
363
364 if child.is_dir():
365 # If gitignore is None, gitignore usage is disabled, while a Falsey
366 # gitignore is when the directory doesn't have a .gitignore file.
367 if gitignore_dict is not None:
368 new_gitignore_dict = {
369 **gitignore_dict,
370 root / child: get_gitignore(child),
371 }
372 else:
373 new_gitignore_dict = None
374 yield from gen_python_files(
375 child.iterdir(),
376 root,
377 include,
378 exclude,
379 extend_exclude,
380 force_exclude,
381 report,
382 new_gitignore_dict,
383 verbose=verbose,
384 quiet=quiet,
385 )
386
387 elif child.is_file():
388 if child.suffix == ".ipynb" and not jupyter_dependencies_are_installed(
389 warn=verbose or not quiet
390 ):
391 continue
392 include_match = include.search(normalized_path) if include else True
393 if include_match:
394 yield child
395
396
397 def wrap_stream_for_windows(
398 f: io.TextIOWrapper,
399 ) -> Union[io.TextIOWrapper, "colorama.AnsiToWin32"]:
400 """
401 Wrap stream with colorama's wrap_stream so colors are shown on Windows.
402
403 If `colorama` is unavailable, the original stream is returned unmodified.
404 Otherwise, the `wrap_stream()` function determines whether the stream needs
405 to be wrapped for a Windows environment and will accordingly either return
406 an `AnsiToWin32` wrapper or the original stream.
407 """
408 try:
409 from colorama.initialise import wrap_stream
410 except ImportError:
411 return f
412 else:
413 # Set `strip=False` to avoid needing to modify test_express_diff_with_color.
414 return wrap_stream(f, convert=None, strip=False, autoreset=False, wrap=True)