Use the separate pass for # fmt: off
on all code
This removes the hacky exception-based handling that didn't work across statement boundaries. Fixes #335
This commit is contained in:
parent
8a8c58252c
commit
df2ae3bbe6
@ -824,7 +824,11 @@ More details can be found in [CONTRIBUTING](CONTRIBUTING.md).
|
|||||||
|
|
||||||
* typing stub files (`.pyi`) now have blank lines added after constants (#340)
|
* typing stub files (`.pyi`) now have blank lines added after constants (#340)
|
||||||
|
|
||||||
* `# fmt: off` and `# fmt: on` now work also within bracket pairs (#329)
|
* `# fmt: off` and `# fmt: on` are now much more dependable:
|
||||||
|
|
||||||
|
* they now work also within bracket pairs (#329)
|
||||||
|
|
||||||
|
* they now correctly work across function/class boundaries (#335)
|
||||||
|
|
||||||
* fixed improper formatting of f-strings with quotes inside interpolated
|
* fixed improper formatting of f-strings with quotes inside interpolated
|
||||||
expressions (#322)
|
expressions (#322)
|
||||||
|
234
black.py
234
black.py
@ -29,7 +29,6 @@
|
|||||||
Sequence,
|
Sequence,
|
||||||
Set,
|
Set,
|
||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
@ -90,34 +89,6 @@ class CannotSplit(Exception):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class FormatError(Exception):
|
|
||||||
"""Base exception for `# fmt: on` and `# fmt: off` handling.
|
|
||||||
|
|
||||||
It holds the number of bytes of the prefix consumed before the format
|
|
||||||
control comment appeared.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, consumed: int) -> None:
|
|
||||||
super().__init__(consumed)
|
|
||||||
self.consumed = consumed
|
|
||||||
|
|
||||||
def trim_prefix(self, leaf: Leaf) -> None:
|
|
||||||
leaf.prefix = leaf.prefix[self.consumed :]
|
|
||||||
|
|
||||||
def leaf_from_consumed(self, leaf: Leaf) -> Leaf:
|
|
||||||
"""Returns a new Leaf from the consumed part of the prefix."""
|
|
||||||
unformatted_prefix = leaf.prefix[: self.consumed]
|
|
||||||
return Leaf(token.NEWLINE, unformatted_prefix)
|
|
||||||
|
|
||||||
|
|
||||||
class FormatOn(FormatError):
|
|
||||||
"""Found a comment like `# fmt: on` in the file."""
|
|
||||||
|
|
||||||
|
|
||||||
class FormatOff(FormatError):
|
|
||||||
"""Found a comment like `# fmt: off` in the file."""
|
|
||||||
|
|
||||||
|
|
||||||
class WriteBack(Enum):
|
class WriteBack(Enum):
|
||||||
NO = 0
|
NO = 0
|
||||||
YES = 1
|
YES = 1
|
||||||
@ -759,13 +730,15 @@ def visit_default(self, node: LN) -> Iterator[T]:
|
|||||||
out(f" {node.value!r}", fg="blue", bold=False)
|
out(f" {node.value!r}", fg="blue", bold=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def show(cls, code: str) -> None:
|
def show(cls, code: Union[str, Leaf, Node]) -> None:
|
||||||
"""Pretty-print the lib2to3 AST of a given string of `code`.
|
"""Pretty-print the lib2to3 AST of a given string of `code`.
|
||||||
|
|
||||||
Convenience method for debugging.
|
Convenience method for debugging.
|
||||||
"""
|
"""
|
||||||
v: DebugVisitor[None] = DebugVisitor()
|
v: DebugVisitor[None] = DebugVisitor()
|
||||||
list(v.visit(lib2to3_parse(code)))
|
if isinstance(code, str):
|
||||||
|
code = lib2to3_parse(code)
|
||||||
|
list(v.visit(code))
|
||||||
|
|
||||||
|
|
||||||
KEYWORDS = set(keyword.kwlist)
|
KEYWORDS = set(keyword.kwlist)
|
||||||
@ -1306,55 +1279,6 @@ def __bool__(self) -> bool:
|
|||||||
return bool(self.leaves or self.comments)
|
return bool(self.leaves or self.comments)
|
||||||
|
|
||||||
|
|
||||||
class UnformattedLines(Line):
|
|
||||||
"""Just like :class:`Line` but stores lines which aren't reformatted."""
|
|
||||||
|
|
||||||
def append(self, leaf: Leaf, preformatted: bool = True) -> None:
|
|
||||||
"""Just add a new `leaf` to the end of the lines.
|
|
||||||
|
|
||||||
The `preformatted` argument is ignored.
|
|
||||||
|
|
||||||
Keeps track of indentation `depth`, which is useful when the user
|
|
||||||
says `# fmt: on`. Otherwise, doesn't do anything with the `leaf`.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
list(generate_comments(leaf))
|
|
||||||
except FormatOn as f_on:
|
|
||||||
self.leaves.append(f_on.leaf_from_consumed(leaf))
|
|
||||||
raise
|
|
||||||
|
|
||||||
self.leaves.append(leaf)
|
|
||||||
if leaf.type == token.INDENT:
|
|
||||||
self.depth += 1
|
|
||||||
elif leaf.type == token.DEDENT:
|
|
||||||
self.depth -= 1
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""Render unformatted lines from leaves which were added with `append()`.
|
|
||||||
|
|
||||||
`depth` is not used for indentation in this case.
|
|
||||||
"""
|
|
||||||
if not self:
|
|
||||||
return "\n"
|
|
||||||
|
|
||||||
res = ""
|
|
||||||
for leaf in self.leaves:
|
|
||||||
res += str(leaf)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def append_comment(self, comment: Leaf) -> bool:
|
|
||||||
"""Not implemented in this class. Raises `NotImplementedError`."""
|
|
||||||
raise NotImplementedError("Unformatted lines don't store comments separately.")
|
|
||||||
|
|
||||||
def maybe_remove_trailing_comma(self, closing: Leaf) -> bool:
|
|
||||||
"""Does nothing and returns False."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool:
|
|
||||||
"""Does nothing and returns False."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EmptyLineTracker:
|
class EmptyLineTracker:
|
||||||
"""Provides a stateful method that returns the number of potential extra
|
"""Provides a stateful method that returns the number of potential extra
|
||||||
@ -1376,9 +1300,6 @@ def maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
|
|||||||
This is for separating `def`, `async def` and `class` with extra empty
|
This is for separating `def`, `async def` and `class` with extra empty
|
||||||
lines (two on module-level).
|
lines (two on module-level).
|
||||||
"""
|
"""
|
||||||
if isinstance(current_line, UnformattedLines):
|
|
||||||
return 0, 0
|
|
||||||
|
|
||||||
before, after = self._maybe_empty_lines(current_line)
|
before, after = self._maybe_empty_lines(current_line)
|
||||||
before -= self.previous_after
|
before -= self.previous_after
|
||||||
self.previous_after = after
|
self.previous_after = after
|
||||||
@ -1482,7 +1403,7 @@ class LineGenerator(Visitor[Line]):
|
|||||||
current_line: Line = Factory(Line)
|
current_line: Line = Factory(Line)
|
||||||
remove_u_prefix: bool = False
|
remove_u_prefix: bool = False
|
||||||
|
|
||||||
def line(self, indent: int = 0, type: Type[Line] = Line) -> Iterator[Line]:
|
def line(self, indent: int = 0) -> Iterator[Line]:
|
||||||
"""Generate a line.
|
"""Generate a line.
|
||||||
|
|
||||||
If the line is empty, only emit if it makes sense.
|
If the line is empty, only emit if it makes sense.
|
||||||
@ -1491,67 +1412,39 @@ def line(self, indent: int = 0, type: Type[Line] = Line) -> Iterator[Line]:
|
|||||||
If any lines were generated, set up a new current_line.
|
If any lines were generated, set up a new current_line.
|
||||||
"""
|
"""
|
||||||
if not self.current_line:
|
if not self.current_line:
|
||||||
if self.current_line.__class__ == type:
|
self.current_line.depth += indent
|
||||||
self.current_line.depth += indent
|
|
||||||
else:
|
|
||||||
self.current_line = type(depth=self.current_line.depth + indent)
|
|
||||||
return # Line is empty, don't emit. Creating a new one unnecessary.
|
return # Line is empty, don't emit. Creating a new one unnecessary.
|
||||||
|
|
||||||
complete_line = self.current_line
|
complete_line = self.current_line
|
||||||
self.current_line = type(depth=complete_line.depth + indent)
|
self.current_line = Line(depth=complete_line.depth + indent)
|
||||||
yield complete_line
|
yield complete_line
|
||||||
|
|
||||||
def visit(self, node: LN) -> Iterator[Line]:
|
|
||||||
"""Main method to visit `node` and its children.
|
|
||||||
|
|
||||||
Yields :class:`Line` objects.
|
|
||||||
"""
|
|
||||||
if isinstance(self.current_line, UnformattedLines):
|
|
||||||
# File contained `# fmt: off`
|
|
||||||
yield from self.visit_unformatted(node)
|
|
||||||
|
|
||||||
else:
|
|
||||||
yield from super().visit(node)
|
|
||||||
|
|
||||||
def visit_default(self, node: LN) -> Iterator[Line]:
|
def visit_default(self, node: LN) -> Iterator[Line]:
|
||||||
"""Default `visit_*()` implementation. Recurses to children of `node`."""
|
"""Default `visit_*()` implementation. Recurses to children of `node`."""
|
||||||
if isinstance(node, Leaf):
|
if isinstance(node, Leaf):
|
||||||
any_open_brackets = self.current_line.bracket_tracker.any_open_brackets()
|
any_open_brackets = self.current_line.bracket_tracker.any_open_brackets()
|
||||||
try:
|
for comment in generate_comments(node):
|
||||||
for comment in generate_comments(node):
|
if any_open_brackets:
|
||||||
if any_open_brackets:
|
# any comment within brackets is subject to splitting
|
||||||
# any comment within brackets is subject to splitting
|
self.current_line.append(comment)
|
||||||
self.current_line.append(comment)
|
elif comment.type == token.COMMENT:
|
||||||
elif comment.type == token.COMMENT:
|
# regular trailing comment
|
||||||
# regular trailing comment
|
self.current_line.append(comment)
|
||||||
self.current_line.append(comment)
|
yield from self.line()
|
||||||
yield from self.line()
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# regular standalone comment
|
# regular standalone comment
|
||||||
yield from self.line()
|
yield from self.line()
|
||||||
|
|
||||||
self.current_line.append(comment)
|
self.current_line.append(comment)
|
||||||
yield from self.line()
|
yield from self.line()
|
||||||
|
|
||||||
except FormatOff as f_off:
|
normalize_prefix(node, inside_brackets=any_open_brackets)
|
||||||
f_off.trim_prefix(node)
|
if self.normalize_strings and node.type == token.STRING:
|
||||||
yield from self.line(type=UnformattedLines)
|
normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix)
|
||||||
yield from self.visit(node)
|
normalize_string_quotes(node)
|
||||||
|
if node.type not in WHITESPACE:
|
||||||
except FormatOn as f_on:
|
self.current_line.append(node)
|
||||||
# This only happens here if somebody says "fmt: on" multiple
|
|
||||||
# times in a row.
|
|
||||||
f_on.trim_prefix(node)
|
|
||||||
yield from self.visit_default(node)
|
|
||||||
|
|
||||||
else:
|
|
||||||
normalize_prefix(node, inside_brackets=any_open_brackets)
|
|
||||||
if self.normalize_strings and node.type == token.STRING:
|
|
||||||
normalize_string_prefix(node, remove_u_prefix=self.remove_u_prefix)
|
|
||||||
normalize_string_quotes(node)
|
|
||||||
if node.type not in WHITESPACE:
|
|
||||||
self.current_line.append(node)
|
|
||||||
yield from super().visit_default(node)
|
yield from super().visit_default(node)
|
||||||
|
|
||||||
def visit_INDENT(self, node: Node) -> Iterator[Line]:
|
def visit_INDENT(self, node: Node) -> Iterator[Line]:
|
||||||
@ -1648,23 +1541,10 @@ def visit_ENDMARKER(self, leaf: Leaf) -> Iterator[Line]:
|
|||||||
yield from self.visit_default(leaf)
|
yield from self.visit_default(leaf)
|
||||||
yield from self.line()
|
yield from self.line()
|
||||||
|
|
||||||
def visit_unformatted(self, node: LN) -> Iterator[Line]:
|
def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]:
|
||||||
"""Used when file contained a `# fmt: off`."""
|
if not self.current_line.bracket_tracker.any_open_brackets():
|
||||||
if isinstance(node, Node):
|
yield from self.line()
|
||||||
for child in node.children:
|
yield from self.visit_default(leaf)
|
||||||
yield from self.visit(child)
|
|
||||||
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self.current_line.append(node)
|
|
||||||
except FormatOn as f_on:
|
|
||||||
f_on.trim_prefix(node)
|
|
||||||
yield from self.line()
|
|
||||||
yield from self.visit(node)
|
|
||||||
|
|
||||||
if node.type == token.ENDMARKER:
|
|
||||||
# somebody decided not to put a final `# fmt: on`
|
|
||||||
yield from self.line()
|
|
||||||
|
|
||||||
def __attrs_post_init__(self) -> None:
|
def __attrs_post_init__(self) -> None:
|
||||||
"""You are in a twisty little maze of passages."""
|
"""You are in a twisty little maze of passages."""
|
||||||
@ -1969,6 +1849,9 @@ def container_of(leaf: Leaf) -> LN:
|
|||||||
if parent.children[0].prefix != same_prefix:
|
if parent.children[0].prefix != same_prefix:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if parent.type == syms.file_input:
|
||||||
|
break
|
||||||
|
|
||||||
if parent.type in SURROUNDED_BY_BRACKETS:
|
if parent.type in SURROUNDED_BY_BRACKETS:
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -2106,16 +1989,6 @@ def generate_comments(leaf: LN) -> Iterator[Leaf]:
|
|||||||
"""
|
"""
|
||||||
for pc in list_comments(leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER):
|
for pc in list_comments(leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER):
|
||||||
yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines)
|
yield Leaf(pc.type, pc.value, prefix="\n" * pc.newlines)
|
||||||
if pc.value in FMT_ON:
|
|
||||||
raise FormatOn(pc.consumed)
|
|
||||||
|
|
||||||
if pc.value in FMT_OFF:
|
|
||||||
if pc.type == STANDALONE_COMMENT:
|
|
||||||
raise FormatOff(pc.consumed)
|
|
||||||
|
|
||||||
prev = preceding_leaf(leaf)
|
|
||||||
if not prev or prev.type in WHITESPACE: # standalone comment in disguise
|
|
||||||
raise FormatOff(pc.consumed)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -2188,7 +2061,7 @@ def split_line(
|
|||||||
If `py36` is True, splitting may generate syntax that is only compatible
|
If `py36` is True, splitting may generate syntax that is only compatible
|
||||||
with Python 3.6 and later.
|
with Python 3.6 and later.
|
||||||
"""
|
"""
|
||||||
if isinstance(line, UnformattedLines) or line.is_comment:
|
if line.is_comment:
|
||||||
yield line
|
yield line
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -2680,28 +2553,29 @@ def normalize_invisible_parens(node: Node, parens_after: Set[str]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def normalize_fmt_off(node: Node) -> None:
|
def normalize_fmt_off(node: Node) -> None:
|
||||||
"""Allow `# fmt: off`/`# fmt: on` within bracket pairs.
|
"""Convert content between `# fmt: off`/`# fmt: on` into standalone comments."""
|
||||||
|
|
||||||
Ignores `# fmt: off` and `# fmt: on` outside of brackets.
|
|
||||||
|
|
||||||
Raises :exc:`SyntaxError` if no matching `# fmt: on` is found for a `# fmt: off`
|
|
||||||
given inside brackets.
|
|
||||||
"""
|
|
||||||
try_again = True
|
try_again = True
|
||||||
while try_again:
|
while try_again:
|
||||||
try_again = hide_fmt_off(node)
|
try_again = convert_one_fmt_off_pair(node)
|
||||||
|
|
||||||
|
|
||||||
def hide_fmt_off(node: Node) -> bool:
|
def convert_one_fmt_off_pair(node: Node) -> bool:
|
||||||
bt = BracketTracker()
|
"""Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment.
|
||||||
|
|
||||||
|
Returns True if a pair was converted.
|
||||||
|
"""
|
||||||
for leaf in node.leaves():
|
for leaf in node.leaves():
|
||||||
bt.mark(leaf)
|
|
||||||
if bt.depth == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
previous_consumed = 0
|
previous_consumed = 0
|
||||||
for comment in list_comments(leaf.prefix, is_endmarker=False):
|
for comment in list_comments(leaf.prefix, is_endmarker=False):
|
||||||
if comment.value in FMT_OFF:
|
if comment.value in FMT_OFF:
|
||||||
|
# We only want standalone comments. If there's no previous leaf or
|
||||||
|
# the previous leaf is indentation, it's a standalone comment in
|
||||||
|
# disguise.
|
||||||
|
if comment.type != STANDALONE_COMMENT:
|
||||||
|
prev = preceding_leaf(leaf)
|
||||||
|
if prev and prev.type not in WHITESPACE:
|
||||||
|
continue
|
||||||
|
|
||||||
ignored_nodes = list(generate_ignored_nodes(leaf))
|
ignored_nodes = list(generate_ignored_nodes(leaf))
|
||||||
first = ignored_nodes[0] # Can be a container node with the `leaf`.
|
first = ignored_nodes[0] # Can be a container node with the `leaf`.
|
||||||
parent = first.parent
|
parent = first.parent
|
||||||
@ -2710,6 +2584,10 @@ def hide_fmt_off(node: Node) -> bool:
|
|||||||
hidden_value = (
|
hidden_value = (
|
||||||
comment.value + "\n" + "".join(str(n) for n in ignored_nodes)
|
comment.value + "\n" + "".join(str(n) for n in ignored_nodes)
|
||||||
)
|
)
|
||||||
|
if hidden_value.endswith("\n"):
|
||||||
|
# That happens when one of the `ignored_nodes` ended with a NEWLINE
|
||||||
|
# leaf (possibly followed by a DEDENT).
|
||||||
|
hidden_value = hidden_value[:-1]
|
||||||
first_idx = None
|
first_idx = None
|
||||||
for ignored in ignored_nodes:
|
for ignored in ignored_nodes:
|
||||||
index = ignored.remove()
|
index = ignored.remove()
|
||||||
@ -2733,8 +2611,12 @@ def hide_fmt_off(node: Node) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def generate_ignored_nodes(leaf: Leaf) -> Iterator[LN]:
|
def generate_ignored_nodes(leaf: Leaf) -> Iterator[LN]:
|
||||||
|
"""Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
|
||||||
|
|
||||||
|
Stops at the end of the block.
|
||||||
|
"""
|
||||||
container: Optional[LN] = container_of(leaf)
|
container: Optional[LN] = container_of(leaf)
|
||||||
while container is not None:
|
while container is not None and container.type != token.ENDMARKER:
|
||||||
for comment in list_comments(container.prefix, is_endmarker=False):
|
for comment in list_comments(container.prefix, is_endmarker=False):
|
||||||
if comment.value in FMT_ON:
|
if comment.value in FMT_ON:
|
||||||
return
|
return
|
||||||
|
31
tests/data/fmtonoff2.py
Normal file
31
tests/data/fmtonoff2.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
TmSt = 1
|
||||||
|
TmEx = 2
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
|
||||||
|
# Test data:
|
||||||
|
# Position, Volume, State, TmSt/TmEx/None, [call, [arg1...]]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('test', [
|
||||||
|
|
||||||
|
# Test don't manage the volume
|
||||||
|
[
|
||||||
|
('stuff', 'in')
|
||||||
|
],
|
||||||
|
])
|
||||||
|
def test_fader(test):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_fader(test):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_calculate_fades():
|
||||||
|
calcs = [
|
||||||
|
# one is zero/none
|
||||||
|
(0, 4, 0, 0, 10, 0, 0, 6, 10),
|
||||||
|
(None, 4, 0, 0, 10, 0, 0, 6, 10),
|
||||||
|
]
|
||||||
|
|
||||||
|
# fmt: on
|
@ -400,6 +400,14 @@ def test_fmtonoff(self) -> None:
|
|||||||
black.assert_equivalent(source, actual)
|
black.assert_equivalent(source, actual)
|
||||||
black.assert_stable(source, actual, line_length=ll)
|
black.assert_stable(source, actual, line_length=ll)
|
||||||
|
|
||||||
|
@patch("black.dump_to_file", dump_to_stderr)
|
||||||
|
def test_fmtonoff2(self) -> None:
|
||||||
|
source, expected = read_data("fmtonoff2")
|
||||||
|
actual = fs(source)
|
||||||
|
self.assertFormatEqual(expected, actual)
|
||||||
|
black.assert_equivalent(source, actual)
|
||||||
|
black.assert_stable(source, actual, line_length=ll)
|
||||||
|
|
||||||
@patch("black.dump_to_file", dump_to_stderr)
|
@patch("black.dump_to_file", dump_to_stderr)
|
||||||
def test_remove_empty_parentheses_after_class(self) -> None:
|
def test_remove_empty_parentheses_after_class(self) -> None:
|
||||||
source, expected = read_data("class_blank_parentheses")
|
source, expected = read_data("class_blank_parentheses")
|
||||||
|
Loading…
Reference in New Issue
Block a user