X-Git-Url: https://git.madduck.net/etc/vim.git/blobdiff_plain/b1d060101626aa1c332f52e4bdf0ae5e4cc07990..a36878eb2f375e2ac1e13052f663909f3835ec46:/src/black/files.py diff --git a/src/black/files.py b/src/black/files.py index ba60c84..ea517f4 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -1,9 +1,10 @@ -from functools import lru_cache import io import os -from pathlib import Path import sys +from functools import lru_cache +from pathlib import Path from typing import ( + TYPE_CHECKING, Any, Dict, Iterable, @@ -14,22 +15,34 @@ from typing import ( Sequence, Tuple, Union, - TYPE_CHECKING, ) +from mypy_extensions import mypyc_attr from pathspec import PathSpec -import tomli +from pathspec.patterns.gitwildmatch import GitWildMatchPatternError +if sys.version_info >= (3, 11): + try: + import tomllib + except ImportError: + # Help users on older alphas + if not TYPE_CHECKING: + import tomli as tomllib +else: + import tomli as tomllib + +from black.handle_ipynb_magics import jupyter_dependencies_are_installed from black.output import err from black.report import Report -from black.handle_ipynb_magics import jupyter_dependencies_are_installed if TYPE_CHECKING: import colorama # noqa: F401 @lru_cache() -def find_project_root(srcs: Sequence[str]) -> Path: +def find_project_root( + srcs: Sequence[str], stdin_filename: Optional[str] = None +) -> Tuple[Path, str]: """Return a directory containing .git, .hg, or pyproject.toml. That directory will be a common parent of all files and directories @@ -37,7 +50,13 @@ def find_project_root(srcs: Sequence[str]) -> Path: If no directory in the tree contains a marker that would specify it's the project root, the root of the file system is returned. + + Returns a two-tuple with the first element as the project root path and + the second element as a string describing the method by which the + project root was discovered. """ + if stdin_filename is not None: + srcs = tuple(stdin_filename if s == "-" else s for s in srcs) if not srcs: srcs = [str(Path.cwd().resolve())] @@ -56,20 +75,20 @@ def find_project_root(srcs: Sequence[str]) -> Path: for directory in (common_base, *common_base.parents): if (directory / ".git").exists(): - return directory + return directory, ".git directory" if (directory / ".hg").is_dir(): - return directory + return directory, ".hg directory" if (directory / "pyproject.toml").is_file(): - return directory + return directory, "pyproject.toml" - return directory + return directory, "file system root" def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: """Find the absolute filepath to a pyproject.toml if it exists""" - path_project_root = find_project_root(path_search_start) + path_project_root, _ = find_project_root(path_search_start) path_pyproject_toml = path_project_root / "pyproject.toml" if path_pyproject_toml.is_file(): return str(path_pyproject_toml) @@ -81,19 +100,20 @@ def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: if path_user_pyproject_toml.is_file() else None ) - except PermissionError as e: + except (PermissionError, RuntimeError) as e: # We do not have access to the user-level config directory, so ignore it. err(f"Ignoring user configuration directory due to {e!r}") return None +@mypyc_attr(patchable=True) def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: """Parse a pyproject toml file, pulling out relevant parts for Black - If parsing fails, will raise a tomli.TOMLDecodeError + If parsing fails, will raise a tomllib.TOMLDecodeError """ - with open(path_config, encoding="utf8") as f: - pyproject_toml = tomli.load(f) # type: ignore # due to deprecated API usage + with open(path_config, "rb") as f: + pyproject_toml = tomllib.load(f) config = pyproject_toml.get("tool", {}).get("black", {}) return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} @@ -104,6 +124,10 @@ def find_user_pyproject_toml() -> Path: This looks for ~\.black on Windows and ~/.config/black on Linux and other Unix systems. + + May raise: + - RuntimeError: if the current user has no homedir + - PermissionError: if the current process cannot access the user's homedir """ if sys.platform == "win32": # Windows @@ -122,11 +146,17 @@ def get_gitignore(root: Path) -> PathSpec: if gitignore.is_file(): with gitignore.open(encoding="utf-8") as gf: lines = gf.readlines() - return PathSpec.from_lines("gitwildmatch", lines) + try: + return PathSpec.from_lines("gitwildmatch", lines) + except GitWildMatchPatternError as e: + err(f"Could not parse {gitignore}: {e}") + raise def normalize_path_maybe_ignore( - path: Path, root: Path, report: Report + path: Path, + root: Path, + report: Optional[Report] = None, ) -> Optional[str]: """Normalize `path`. May return `None` if `path` was ignored. @@ -134,19 +164,35 @@ def normalize_path_maybe_ignore( """ try: abspath = path if path.is_absolute() else Path.cwd() / path - normalized_path = abspath.resolve().relative_to(root).as_posix() + normalized_path = abspath.resolve() + try: + root_relative_path = normalized_path.relative_to(root).as_posix() + except ValueError: + if report: + report.path_ignored( + path, f"is a symbolic link that points outside {root}" + ) + return None + except OSError as e: - report.path_ignored(path, f"cannot be read because {e}") + if report: + report.path_ignored(path, f"cannot be read because {e}") return None - except ValueError: - if path.is_symlink(): - report.path_ignored(path, f"is a symbolic link that points outside {root}") - return None + return root_relative_path - raise - return normalized_path +def path_is_ignored( + path: Path, gitignore_dict: Dict[Path, PathSpec], report: Report +) -> bool: + for gitignore_path, pattern in gitignore_dict.items(): + relative_path = normalize_path_maybe_ignore(path, gitignore_path, report) + if relative_path is None: + break + if pattern.match_file(relative_path): + report.path_ignored(path, "matches a .gitignore file content") + return True + return False def path_is_excluded( @@ -165,7 +211,7 @@ def gen_python_files( extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], report: Report, - gitignore: Optional[PathSpec], + gitignore_dict: Optional[Dict[Path, PathSpec]], *, verbose: bool, quiet: bool, @@ -178,6 +224,7 @@ def gen_python_files( `report` is where output about exclusions goes. """ + assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}" for child in paths: normalized_path = normalize_path_maybe_ignore(child, root, report) @@ -185,8 +232,7 @@ def gen_python_files( continue # First ignore files matching .gitignore, if passed - if gitignore is not None and gitignore.match_file(normalized_path): - report.path_ignored(child, "matches the .gitignore file content") + if gitignore_dict and path_is_ignored(child, gitignore_dict, report): continue # Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options. @@ -211,6 +257,13 @@ def gen_python_files( if child.is_dir(): # If gitignore is None, gitignore usage is disabled, while a Falsey # gitignore is when the directory doesn't have a .gitignore file. + if gitignore_dict is not None: + new_gitignore_dict = { + **gitignore_dict, + root / child: get_gitignore(child), + } + else: + new_gitignore_dict = None yield from gen_python_files( child.iterdir(), root, @@ -219,7 +272,7 @@ def gen_python_files( extend_exclude, force_exclude, report, - gitignore + get_gitignore(child) if gitignore is not None else None, + new_gitignore_dict, verbose=verbose, quiet=quiet, )