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):
"--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(
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"
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.
+ # Leave octal, hex, and binary literals alone.
pass
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
# No underscores for numbers <= 6 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