import click
import toml
from typed_ast import ast3, ast27
+from pathspec import PathSpec
# lib2to3 fork
from blib2to3.pytree import Node, Leaf, type_repr
from blib2to3.pgen2.grammar import Grammar
from blib2to3.pgen2.parse import ParseError
-from _version import version as __version__
+from _black_version import version as __version__
DEFAULT_LINE_LENGTH = 88
DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950
p = Path(s)
if p.is_dir():
sources.update(
- gen_python_files_in_dir(p, root, include_regex, exclude_regex, report)
+ gen_python_files_in_dir(
+ p, root, include_regex, exclude_regex, report, get_gitignore(root)
+ )
)
elif p.is_file() or s == "-":
# if a file was explicitly given, we don't care about its extension
self.current_line.append(node)
yield from super().visit_default(node)
- def visit_atom(self, node: Node) -> Iterator[Line]:
- # Always make parentheses invisible around a single node, because it should
- # not be needed (except in the case of yield, where removing the parentheses
- # produces a SyntaxError).
- if (
- len(node.children) == 3
- and isinstance(node.children[0], Leaf)
- and node.children[0].type == token.LPAR
- and isinstance(node.children[2], Leaf)
- and node.children[2].type == token.RPAR
- and isinstance(node.children[1], Leaf)
- and not (
- node.children[1].type == token.NAME
- and node.children[1].value == "yield"
- )
- ):
- node.children[0].value = ""
- node.children[2].value = ""
- yield from super().visit_default(node)
-
- def visit_factor(self, node: Node) -> Iterator[Line]:
- """Force parentheses between a unary op and a binary power:
-
- -2 ** 8 -> -(2 ** 8)
- """
- child = node.children[1]
- if child.type == syms.power and len(child.children) == 3:
- lpar = Leaf(token.LPAR, "(")
- rpar = Leaf(token.RPAR, ")")
- index = child.remove() or 0
- node.insert_child(index, Node(syms.atom, [lpar, child, rpar]))
- yield from self.visit_default(node)
-
def visit_INDENT(self, node: Node) -> Iterator[Line]:
"""Increase indentation level, maybe yield a line."""
# In blib2to3 INDENT never holds comments.
yield from self.line()
yield from self.visit_default(leaf)
+ def visit_factor(self, node: Node) -> Iterator[Line]:
+ """Force parentheses between a unary op and a binary power:
+
+ -2 ** 8 -> -(2 ** 8)
+ """
+ _operator, operand = node.children
+ if (
+ operand.type == syms.power
+ and len(operand.children) == 3
+ and operand.children[1].type == token.DOUBLESTAR
+ ):
+ lpar = Leaf(token.LPAR, "(")
+ rpar = Leaf(token.RPAR, ")")
+ index = operand.remove() or 0
+ node.insert_child(index, Node(syms.atom, [lpar, operand, rpar]))
+ yield from self.visit_default(node)
+
def __attrs_post_init__(self) -> None:
"""You are in a twisty little maze of passages."""
v = self.visit_stmt
# Since body is a new indent level, remove spurious leading whitespace.
normalize_prefix(leaves[0], inside_brackets=True)
# Ensure a trailing comma for imports and standalone function arguments, but
- # be careful not to add one after any comments.
- no_commas = original.is_def and not any(
- l.type == token.COMMA for l in leaves
+ # be careful not to add one after any comments or within type annotations.
+ no_commas = (
+ original.is_def
+ and opening_bracket.value == "("
+ and not any(l.type == token.COMMA for l in leaves)
)
if original.is_import or no_commas:
Only returns true for type comments for now."""
t = leaf.type
v = leaf.value
- return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith(
- "# type:" + suffix
- )
+ return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix)
def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None:
return imports
+@lru_cache()
+def get_gitignore(root: Path) -> PathSpec:
+ """ Return a PathSpec matching gitignore content if present."""
+ gitignore = root / ".gitignore"
+ if not gitignore.is_file():
+ return PathSpec.from_lines("gitwildmatch", [])
+ else:
+ return PathSpec.from_lines("gitwildmatch", gitignore.open())
+
+
def gen_python_files_in_dir(
path: Path,
root: Path,
include: Pattern[str],
exclude: Pattern[str],
report: "Report",
+ gitignore: PathSpec,
) -> Iterator[Path]:
"""Generate all files under `path` whose paths are not excluded by the
`exclude` regex, but are included by the `include` regex.
"""
assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
for child in path.iterdir():
+ # First ignore files matching .gitignore
+ if gitignore.match_file(child.as_posix()):
+ report.path_ignored(child, f"matches the .gitignore file content")
+ continue
+
+ # Then ignore with `exclude` option.
try:
normalized_path = "/" + child.resolve().relative_to(root).as_posix()
+ except OSError as e:
+ report.path_ignored(child, f"cannot be read because {e}")
+ continue
except ValueError:
if child.is_symlink():
report.path_ignored(
if child.is_dir():
normalized_path += "/"
+
exclude_match = exclude.search(normalized_path)
if exclude_match and exclude_match.group(0):
report.path_ignored(child, f"matches the --exclude regular expression")
continue
if child.is_dir():
- yield from gen_python_files_in_dir(child, root, include, exclude, report)
+ yield from gen_python_files_in_dir(
+ child, root, include, exclude, report, gitignore
+ )
elif child.is_file():
include_match = include.search(normalized_path)
@contextmanager
def nullcontext() -> Iterator[None]:
- """Return context manager that does nothing.
- Similar to `nullcontext` from python 3.7"""
+ """Return an empty context manager.
+
+ To be used like `nullcontext` in Python 3.7.
+ """
yield
with cache_file.open("rb") as fobj:
try:
cache: Cache = pickle.load(fobj)
- except pickle.UnpicklingError:
+ except (pickle.UnpicklingError, ValueError):
return {}
return cache
CACHE_DIR.mkdir(parents=True, exist_ok=True)
new_cache = {**cache, **{src.resolve(): get_cache_info(src) for src in sources}}
with tempfile.NamedTemporaryFile(dir=str(cache_file.parent), delete=False) as f:
- pickle.dump(new_cache, f, protocol=pickle.HIGHEST_PROTOCOL)
+ pickle.dump(new_cache, f, protocol=4)
os.replace(f.name, cache_file)
except OSError:
pass