import os
from pathlib import Path
import pickle
-import re
+import regex as re
import signal
import sys
import tempfile
from blib2to3.pgen2.grammar import Grammar
from blib2to3.pgen2.parse import ParseError
-from _version import get_versions
-
-v = get_versions()
-__version__ = v.get("closest-tag", v["version"])
-__git_version__ = v.get("full-revisionid")
+from _version import version as __version__
DEFAULT_LINE_LENGTH = 88
-DEFAULT_EXCLUDES = (
- r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist)/"
-)
+DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950
DEFAULT_INCLUDES = r"\.pyi?$"
-CACHE_DIR = Path(user_cache_dir("black", version=__git_version__))
+CACHE_DIR = Path(user_cache_dir("black", version=__version__))
# types
Leaf(token.DOT, ".") for _ in range(3)
]
+ @property
+ def is_collection_with_optional_trailing_comma(self) -> bool:
+ """Is this line a collection literal with a trailing comma that's optional?
+
+ Note that the trailing comma in a 1-tuple is not optional.
+ """
+ if not self.leaves or len(self.leaves) < 4:
+ return False
+ # Look for and address a trailing colon.
+ if self.leaves[-1].type == token.COLON:
+ closer = self.leaves[-2]
+ close_index = -2
+ else:
+ closer = self.leaves[-1]
+ close_index = -1
+ if closer.type not in CLOSING_BRACKETS or self.inside_brackets:
+ return False
+ if closer.type == token.RPAR:
+ # Tuples require an extra check, because if there's only
+ # one element in the tuple removing the comma unmakes the
+ # tuple.
+ #
+ # We also check for parens before looking for the trailing
+ # comma because in some cases (eg assigning a dict
+ # literal) the literal gets wrapped in temporary parens
+ # during parsing. This case is covered by the
+ # collections.py test data.
+ opener = closer.opening_bracket
+ for _open_index, leaf in enumerate(self.leaves):
+ if leaf is opener:
+ break
+ else:
+ # Couldn't find the matching opening paren, play it safe.
+ return False
+ commas = 0
+ comma_depth = self.leaves[close_index - 1].bracket_depth
+ for leaf in self.leaves[_open_index + 1 : close_index]:
+ if leaf.bracket_depth == comma_depth and leaf.type == token.COMMA:
+ commas += 1
+ if commas > 1:
+ # We haven't looked yet for the trailing comma because
+ # we might also have caught noop parens.
+ return self.leaves[close_index - 1].type == token.COMMA
+ elif commas == 1:
+ return False # it's either a one-tuple or didn't have a trailing comma
+ if self.leaves[close_index - 1].type in CLOSING_BRACKETS:
+ close_index -= 1
+ closer = self.leaves[close_index]
+ if closer.type == token.RPAR:
+ # TODO: this is a gut feeling. Will we ever see this?
+ return False
+ if self.leaves[close_index - 1].type != token.COMMA:
+ return False
+ return True
+
@property
def is_def(self) -> bool:
"""Is this a function definition? (Also returns True for async defs.)"""
return True
return False
- def contains_inner_type_comments(self) -> bool:
+ def contains_uncollapsable_type_comments(self) -> bool:
ignored_ids = set()
try:
last_leaf = self.leaves[-1]
except IndexError:
return False
+ # A type comment is uncollapsable if it is attached to a leaf
+ # that isn't at the end of the line (since that could cause it
+ # to get associated to a different argument) or if there are
+ # comments before it (since that could cause it to get hidden
+ # behind a comment.
+ comment_seen = False
for leaf_id, comments in self.comments.items():
- if leaf_id in ignored_ids:
- continue
-
for comment in comments:
if is_type_comment(comment):
- return True
+ if leaf_id not in ignored_ids or comment_seen:
+ return True
+
+ comment_seen = True
+
+ return False
+
+ def contains_unsplittable_type_ignore(self) -> bool:
+ if not self.leaves:
+ return False
+
+ # If a 'type: ignore' is attached to the end of a line, we
+ # can't split the line, because we can't know which of the
+ # subexpressions the ignore was meant to apply to.
+ #
+ # We only want this to apply to actual physical lines from the
+ # original source, though: we don't want the presence of a
+ # 'type: ignore' at the end of a multiline expression to
+ # justify pushing it all onto one line. Thus we
+ # (unfortunately) need to check the actual source lines and
+ # only report an unsplittable 'type: ignore' if this line was
+ # one line in the original code.
+
+ # Grab the first and last line numbers, skipping generated leaves
+ first_line = next((l.lineno for l in self.leaves if l.lineno != 0), 0)
+ last_line = next((l.lineno for l in reversed(self.leaves) if l.lineno != 0), 0)
+
+ if first_line == last_line:
+ # We look at the last two leaves since a comma or an
+ # invisible paren could have been added at the end of the
+ # line.
+ for node in self.leaves[-2:]:
+ for comment in self.comments.get(id(node), []):
+ if is_type_comment(comment, " ignore"):
+ return True
return False
def maybe_remove_trailing_comma(self, closing: Leaf) -> bool:
"""Remove trailing comma if there is one and it's safe."""
+ if not (self.leaves and self.leaves[-1].type == token.COMMA):
+ return False
+ # We remove trailing commas only in the case of importing a
+ # single name from a module.
if not (
self.leaves
+ and self.is_import
+ and len(self.leaves) > 4
and self.leaves[-1].type == token.COMMA
and closing.type in CLOSING_BRACKETS
+ and self.leaves[-4].type == token.NAME
+ and (
+ # regular `from foo import bar,`
+ self.leaves[-4].value == "import"
+ # `from foo import (bar as baz,)
+ or (
+ len(self.leaves) > 6
+ and self.leaves[-6].value == "import"
+ and self.leaves[-3].value == "as"
+ )
+ # `from foo import bar as baz,`
+ or (
+ len(self.leaves) > 5
+ and self.leaves[-5].value == "import"
+ and self.leaves[-3].value == "as"
+ )
+ )
+ and closing.type == token.RPAR
):
return False
- if closing.type == token.RBRACE:
- self.remove_trailing_comma()
- return True
-
- if closing.type == token.RSQB:
- comma = self.leaves[-1]
- if comma.parent and comma.parent.type == syms.listmaker:
- self.remove_trailing_comma()
- return True
-
- # For parens let's check if it's safe to remove the comma.
- # Imports are always safe.
- if self.is_import:
- self.remove_trailing_comma()
- return True
-
- # Otherwise, if the trailing one is the only one, we might mistakenly
- # change a tuple into a different type by removing the comma.
- depth = closing.bracket_depth + 1
- commas = 0
- opening = closing.opening_bracket
- for _opening_index, leaf in enumerate(self.leaves):
- if leaf is opening:
- break
-
- else:
- return False
-
- for leaf in self.leaves[_opening_index + 1 :]:
- if leaf is closing:
- break
-
- bracket_depth = leaf.bracket_depth
- if bracket_depth == depth and leaf.type == token.COMMA:
- commas += 1
- if leaf.parent and leaf.parent.type in {
- syms.arglist,
- syms.typedargslist,
- }:
- commas += 1
- break
-
- if commas > 1:
- self.remove_trailing_comma()
- return True
-
- return False
+ self.remove_trailing_comma()
+ return True
def append_comment(self, comment: Leaf) -> bool:
"""Add an inline or standalone comment to the line."""
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:
line_str = str(line).strip("\n")
if (
- not line.contains_inner_type_comments()
+ not line.contains_uncollapsable_type_comments()
and not line.should_explode
- and is_line_short_enough(line, line_length=line_length, line_str=line_str)
+ and not line.is_collection_with_optional_trailing_comma
+ and (
+ is_line_short_enough(line, line_length=line_length, line_str=line_str)
+ or line.contains_unsplittable_type_ignore()
+ )
):
yield line
return
)
-def is_type_comment(leaf: Leaf) -> bool:
+def is_type_comment(leaf: Leaf, suffix: str = "") -> bool:
"""Return True if the given leaf is a special comment.
Only returns true for type comments for now."""
t = leaf.type
v = leaf.value
- return t in {token.COMMENT, t == STANDALONE_COMMENT} and v.startswith("# type:")
+ return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:" + suffix)
def normalize_prefix(leaf: Leaf, *, inside_brackets: bool) -> None:
"""
if "\n" in regex:
regex = "(?x)" + regex
- return re.compile(regex)
+ compiled: Pattern[str] = re.compile(regex)
+ return compiled
def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]: