__version__ = "18.6b4"
DEFAULT_LINE_LENGTH = 88
DEFAULT_EXCLUDES = (
- r"/(\.git|\.hg|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist)/"
+ r"/(\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist)/"
)
DEFAULT_INCLUDES = r"\.pyi?$"
CACHE_DIR = Path(user_cache_dir("black", version=__version__))
class NothingChanged(UserWarning):
- """Raised by :func:`format_file` when reformatted code is the same as source."""
+ """Raised when reformatted code is the same as source."""
class CannotSplit(Exception):
- """A readable split that fits the allotted line length is impossible.
+ """A readable split that fits the allotted line length is impossible."""
- Raised by :func:`left_hand_split`, :func:`right_hand_split`, and
- :func:`delimiter_split`.
- """
+
+class InvalidInput(ValueError):
+ """Raised when input source code fails all parse attempts."""
class WriteBack(Enum):
PYTHON36 = 1
PYI = 2
NO_STRING_NORMALIZATION = 4
+ NO_NUMERIC_UNDERSCORE_NORMALIZATION = 8
@classmethod
def from_configuration(
- cls, *, py36: bool, pyi: bool, skip_string_normalization: bool
+ cls,
+ *,
+ py36: bool,
+ pyi: bool,
+ skip_string_normalization: bool,
+ skip_numeric_underscore_normalization: bool,
) -> "FileMode":
mode = cls.AUTO_DETECT
if py36:
mode |= cls.PYI
if skip_string_normalization:
mode |= cls.NO_STRING_NORMALIZATION
+ if skip_numeric_underscore_normalization:
+ mode |= cls.NO_NUMERIC_UNDERSCORE_NORMALIZATION
return mode
"--line-length",
type=int,
default=DEFAULT_LINE_LENGTH,
- help="How many character per line to allow.",
+ help="How many characters per line to allow.",
show_default=True,
)
@click.option(
is_flag=True,
help="Don't normalize string quotes or prefixes.",
)
+@click.option(
+ "-N",
+ "--skip-numeric-underscore-normalization",
+ is_flag=True,
+ help="Don't normalize underscores in numeric literals.",
+)
@click.option(
"--check",
is_flag=True,
pyi: bool,
py36: bool,
skip_string_normalization: bool,
+ skip_numeric_underscore_normalization: bool,
quiet: bool,
verbose: bool,
include: str,
"""The uncompromising code formatter."""
write_back = WriteBack.from_configuration(check=check, diff=diff)
mode = FileMode.from_configuration(
- py36=py36, pyi=pyi, skip_string_normalization=skip_string_normalization
+ py36=py36,
+ pyi=pyi,
+ skip_string_normalization=skip_string_normalization,
+ skip_numeric_underscore_normalization=skip_numeric_underscore_normalization,
)
if config and verbose:
out(f"Using configuration from {config}.", bold=False, fg="blue")
remove_u_prefix=py36 or "unicode_literals" in future_imports,
is_pyi=is_pyi,
normalize_strings=normalize_strings,
- allow_underscores=py36,
+ allow_underscores=py36
+ and not bool(mode & FileMode.NO_NUMERIC_UNDERSCORE_NORMALIZATION),
)
elt = EmptyLineTracker(is_pyi=is_pyi)
empty_line = Line()
faulty_line = lines[lineno - 1]
except IndexError:
faulty_line = "<line number missing in source>"
- exc = ValueError(f"Cannot parse: {lineno}:{column}: {faulty_line}")
+ exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {faulty_line}")
else:
raise exc from None
bracket_match: Dict[Tuple[Depth, NodeType], Leaf] = Factory(dict)
delimiters: Dict[LeafID, Priority] = Factory(dict)
previous: Optional[Leaf] = None
- _for_loop_variable: int = 0
- _lambda_arguments: int = 0
+ _for_loop_depths: List[int] = Factory(list)
+ _lambda_argument_depths: List[int] = Factory(list)
def mark(self, leaf: Leaf) -> None:
"""Mark `leaf` with bracket-related metadata. Keep track of delimiters.
"""
if leaf.type == token.NAME and leaf.value == "for":
self.depth += 1
- self._for_loop_variable += 1
+ self._for_loop_depths.append(self.depth)
return True
return False
def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool:
"""See `maybe_increment_for_loop_variable` above for explanation."""
- if self._for_loop_variable and leaf.type == token.NAME and leaf.value == "in":
+ if (
+ self._for_loop_depths
+ and self._for_loop_depths[-1] == self.depth
+ and leaf.type == token.NAME
+ and leaf.value == "in"
+ ):
self.depth -= 1
- self._for_loop_variable -= 1
+ self._for_loop_depths.pop()
return True
return False
"""
if leaf.type == token.NAME and leaf.value == "lambda":
self.depth += 1
- self._lambda_arguments += 1
+ self._lambda_argument_depths.append(self.depth)
return True
return False
def maybe_decrement_after_lambda_arguments(self, leaf: Leaf) -> bool:
"""See `maybe_increment_lambda_arguments` above for explanation."""
- if self._lambda_arguments and leaf.type == token.COLON:
+ if (
+ self._lambda_argument_depths
+ and self._lambda_argument_depths[-1] == self.depth
+ and leaf.type == token.COLON
+ ):
self.depth -= 1
- self._lambda_arguments -= 1
+ self._lambda_argument_depths.pop()
return True
return False
self.remove_trailing_comma()
return True
- # Otheriwsse, if the trailing one is the only one, we might mistakenly
+ # 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
newlines = 1
elif current_line.is_class or self.previous_line.is_class:
if current_line.is_stub_class and self.previous_line.is_stub_class:
- # No blank line between classes with an emty body
+ # No blank line between classes with an empty body
newlines = 0
else:
newlines = 1
def is_split_before_delimiter(leaf: Leaf, previous: Leaf = None) -> int:
- """Return the priority of the `leaf` delimiter, given a line before after it.
+ """Return the priority of the `leaf` delimiter, given a line break before it.
The delimiter priorities returned here are from those delimiters that would
cause a line break before themselves.
):
return STRING_PRIORITY
- if leaf.type != token.NAME:
+ if leaf.type not in {token.NAME, token.ASYNC}:
return 0
if (
leaf.value == "for"
and leaf.parent
and leaf.parent.type in {syms.comp_for, syms.old_comp_for}
+ or leaf.type == token.ASYNC
):
- return COMPREHENSION_PRIORITY
+ if (
+ not isinstance(leaf.prev_sibling, Leaf)
+ or leaf.prev_sibling.value != "async"
+ ):
+ return COMPREHENSION_PRIORITY
if (
leaf.value == "if"
@dataclass
class ProtoComment:
+ """Describes a piece of syntax that is a comment.
+
+ It's not a :class:`blib2to3.pytree.Leaf` so that:
+
+ * it can be cached (`Leaf` objects should not be reused more than once as
+ they store their lineno, column, prefix, and parent information);
+ * `newlines` and `consumed` fields are kept separate from the `value`. This
+ simplifies handling of special marker comments like ``# fmt: off/on``.
+ """
+
type: int # token.COMMENT or STANDALONE_COMMENT
value: str # content of the comment
newlines: int # how many newlines before the comment
@lru_cache(maxsize=4096)
def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]:
+ """Return a list of :class:`ProtoComment` objects parsed from the given `prefix`."""
result: List[ProtoComment] = []
if not prefix or "#" not in prefix:
return result
def normalize_numeric_literal(leaf: Leaf, allow_underscores: bool) -> None:
- """Normalizes numeric (float, int, and complex) literals."""
- # We want all letters (e in exponents, j in complex literals, a-f
- # in hex literals) to be lowercase.
+ """Normalizes numeric (float, int, and complex) literals.
+
+ All letters used in the representation are normalized to lowercase (except
+ in Python 2 long literals), and long number literals are split using underscores.
+ """
text = leaf.value.lower()
- if text.startswith(("0o", "0x", "0b")):
- # Leave octal, hex, and binary literals alone for now.
+ if text.startswith(("0o", "0b")):
+ # Leave octal and binary literals alone.
pass
+ elif text.startswith("0x"):
+ # Change hex literals to upper case.
+ before, after = text[:2], text[2:]
+ text = f"{before}{after.upper()}"
elif "e" in text:
before, after = text.split("e")
+ sign = ""
if after.startswith("-"):
after = after[1:]
sign = "-"
elif after.startswith("+"):
after = after[1:]
- sign = ""
- else:
- sign = ""
before = format_float_or_int_string(before, allow_underscores)
after = format_int_string(after, allow_underscores)
text = f"{before}e{sign}{after}"
- # Complex numbers and Python 2 longs
- elif "j" in text or "l" in text:
+ elif text.endswith(("j", "l")):
number = text[:-1]
suffix = text[-1]
+ # Capitalize in "2L" because "l" looks too similar to "1".
+ if suffix == "l":
+ suffix = "L"
text = f"{format_float_or_int_string(number, allow_underscores)}{suffix}"
else:
text = format_float_or_int_string(text, allow_underscores)
before, after = text.split(".")
before = format_int_string(before, allow_underscores) if before else "0"
- after = format_int_string(after, allow_underscores) if after else "0"
+ if after:
+ after = format_int_string(after, allow_underscores, count_from_end=False)
+ else:
+ after = "0"
return f"{before}.{after}"
-def format_int_string(text: str, allow_underscores: bool) -> str:
+def format_int_string(
+ text: str, allow_underscores: bool, count_from_end: bool = True
+) -> str:
"""Normalizes underscores in a string to e.g. 1_000_000.
- Input must be a string consisting only of digits and underscores.
+ Input must be a string of digits and optional underscores.
+ If count_from_end is False, we add underscores after groups of three digits
+ counting from the beginning instead of the end of the strings. This is used
+ for the fractional part of float literals.
"""
if not allow_underscores:
return text
text = text.replace("_", "")
- if len(text) <= 6:
- # No underscores for numbers <= 6 digits long.
+ if len(text) <= 5:
+ # No underscores for numbers <= 5 digits long.
return text
- return format(int(text), "3_")
+ if count_from_end:
+ # Avoid removing leading zeros, which are important if we're formatting
+ # part of a number like "0.001".
+ return format(int("1" + text), "3_")[1:].lstrip("_")
+ else:
+ return "_".join(text[i : i + 3] for i in range(0, len(text), 3))
def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None:
for index, child in enumerate(list(node.children)):
if check_lpar:
if child.type == syms.atom:
- maybe_make_parens_invisible_in_atom(child)
+ if maybe_make_parens_invisible_in_atom(child):
+ lpar = Leaf(token.LPAR, "")
+ rpar = Leaf(token.RPAR, "")
+ index = child.remove() or 0
+ node.insert_child(index, Node(syms.atom, [lpar, child, rpar]))
elif is_one_tuple(child):
# wrap child in visible parentheses
lpar = Leaf(token.LPAR, "(")
def maybe_make_parens_invisible_in_atom(node: LN) -> bool:
- """If it's safe, make the parens in the atom `node` invisible, recursively."""
+ """If it's safe, make the parens in the atom `node` invisible, recursively.
+
+ Returns whether the node should itself be wrapped in invisible parentheses.
+
+ """
if (
node.type != syms.atom
or is_empty_tuple(node)
last.value = "" # type: ignore
if len(node.children) > 1:
maybe_make_parens_invisible_in_atom(node.children[1])
- return True
+ return False
- return False
+ return True
def is_empty_tuple(node: LN) -> bool:
"""Return True if the current file is using Python 3.6+ features.
Currently looking for:
- - f-strings; and
+ - f-strings;
+ - underscores in numeric literals; and
- trailing commas after * or ** in function signatures and calls.
"""
for n in node.pre_order():
if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}:
return True
+ elif n.type == token.NUMBER:
+ if "_" in n.value: # type: ignore
+ return True
+
elif (
n.type in {syms.typedargslist, syms.arglist}
and n.children
length = 4 * line.depth
opening_bracket = None
closing_bracket = None
- optional_brackets: Set[LeafID] = set()
inner_brackets: Set[LeafID] = set()
for index, leaf, leaf_length in enumerate_with_length(line, reversed=True):
length += leaf_length
if leaf.type == STANDALONE_COMMENT or has_inline_comment:
break
- optional_brackets.discard(id(leaf))
if opening_bracket:
if leaf is opening_bracket:
opening_bracket = None
elif leaf.type in CLOSING_BRACKETS:
inner_brackets.add(id(leaf))
elif leaf.type in CLOSING_BRACKETS:
- if not leaf.value:
- optional_brackets.add(id(opening_bracket))
- continue
-
if index > 0 and line.leaves[index - 1].type in OPENING_BRACKETS:
# Empty brackets would fail a split so treat them as "inner"
# brackets (e.g. only add them to the `omit` set if another
inner_brackets.add(id(leaf))
continue
- opening_bracket = leaf.opening_bracket
if closing_bracket:
omit.add(id(closing_bracket))
omit.update(inner_brackets)
inner_brackets.clear()
yield omit
- closing_bracket = leaf
+
+ if leaf.value:
+ opening_bracket = leaf.opening_bracket
+ closing_bracket = leaf
def get_future_imports(node: Node) -> Set[str]:
- otherwise return 0.
"""
# According to http://tldp.org/LDP/abs/html/exitcodes.html starting with
- # 126 we have special returncodes reserved by the shell.
+ # 126 we have special return codes reserved by the shell.
if self.failure_count:
return 123