Skip to content

Commit

Permalink
Add "# fmt: skip" directive to black (#1800)
Browse files Browse the repository at this point in the history
Fixes #1162
  • Loading branch information
saroad2 committed Feb 15, 2021
1 parent 3a309a3 commit 6a105e0
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 42 deletions.
5 changes: 3 additions & 2 deletions README.md
Expand Up @@ -239,8 +239,9 @@ feeling confident, use `--fast`.
_Black_ is a PEP 8 compliant opinionated formatter. _Black_ reformats entire files in
place. It is not configurable. It doesn't take previous formatting into account. Your
main option of configuring _Black_ is that it doesn't reformat blocks that start with
`# fmt: off` and end with `# fmt: on`. `# fmt: on/off` have to be on the same level of
indentation. To learn more about _Black_'s opinions, to go
`# fmt: off` and end with `# fmt: on`, or lines that ends with `# fmt: skip`. Pay
attention that `# fmt: on/off` have to be on the same level of indentation. To learn
more about _Black_'s opinions, to go
[the_black_code_style](https://github.com/psf/black/blob/master/docs/the_black_code_style.md).

Please refer to this document before submitting an issue. What seems like a bug might be
Expand Down
104 changes: 64 additions & 40 deletions src/black/__init__.py
Expand Up @@ -2581,6 +2581,8 @@ def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Pr


FMT_OFF = {"# fmt: off", "# fmt:off", "# yapf: disable"}
FMT_SKIP = {"# fmt: skip", "# fmt:skip"}
FMT_PASS = {*FMT_OFF, *FMT_SKIP}
FMT_ON = {"# fmt: on", "# fmt:on", "# yapf: enable"}


Expand Down Expand Up @@ -5404,58 +5406,80 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
for leaf in node.leaves():
previous_consumed = 0
for comment in list_comments(leaf.prefix, is_endmarker=False):
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:
if comment.value not in FMT_PASS:
previous_consumed = comment.consumed
continue
# 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.value in FMT_PASS and comment.type != STANDALONE_COMMENT:
prev = preceding_leaf(leaf)
if prev:
if comment.value in FMT_OFF and prev.type not in WHITESPACE:
continue
if comment.value in FMT_SKIP and prev.type in WHITESPACE:
continue

ignored_nodes = list(generate_ignored_nodes(leaf))
if not ignored_nodes:
continue

first = ignored_nodes[0] # Can be a container node with the `leaf`.
parent = first.parent
prefix = first.prefix
first.prefix = prefix[comment.consumed :]
hidden_value = (
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: Optional[int] = None
for ignored in ignored_nodes:
index = ignored.remove()
if first_idx is None:
first_idx = index
assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)"
assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)"
parent.insert_child(
first_idx,
Leaf(
STANDALONE_COMMENT,
hidden_value,
prefix=prefix[:previous_consumed] + "\n" * comment.newlines,
),
)
return True
ignored_nodes = list(generate_ignored_nodes(leaf, comment))
if not ignored_nodes:
continue

previous_consumed = comment.consumed
first = ignored_nodes[0] # Can be a container node with the `leaf`.
parent = first.parent
prefix = first.prefix
first.prefix = prefix[comment.consumed :]
hidden_value = "".join(str(n) for n in ignored_nodes)
if comment.value in FMT_OFF:
hidden_value = comment.value + "\n" + hidden_value
if comment.value in FMT_SKIP:
hidden_value += " " + comment.value
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: Optional[int] = None
for ignored in ignored_nodes:
index = ignored.remove()
if first_idx is None:
first_idx = index
assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)"
assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)"
parent.insert_child(
first_idx,
Leaf(
STANDALONE_COMMENT,
hidden_value,
prefix=prefix[:previous_consumed] + "\n" * comment.newlines,
),
)
return True

return False


def generate_ignored_nodes(leaf: Leaf) -> Iterator[LN]:
def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]:
"""Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
If comment is skip, returns leaf only.
Stops at the end of the block.
"""
container: Optional[LN] = container_of(leaf)
if comment.value in FMT_SKIP:
prev_sibling = leaf.prev_sibling
if comment.value in leaf.prefix and prev_sibling is not None:
leaf.prefix = leaf.prefix.replace(comment.value, "")
siblings = [prev_sibling]
while (
"\n" not in prev_sibling.prefix
and prev_sibling.prev_sibling is not None
):
prev_sibling = prev_sibling.prev_sibling
siblings.insert(0, prev_sibling)
for sibling in siblings:
yield sibling
elif leaf.parent is not None:
yield leaf.parent
return
while container is not None and container.type != token.ENDMARKER:
if is_fmt_on(container):
return
Expand Down
3 changes: 3 additions & 0 deletions tests/data/fmtskip.py
@@ -0,0 +1,3 @@
a, b = 1, 2
c = 6 # fmt: skip
d = 5
17 changes: 17 additions & 0 deletions tests/data/fmtskip2.py
@@ -0,0 +1,17 @@
l1 = ["This list should be broken up", "into multiple lines", "because it is way too long"]
l2 = ["But this list shouldn't", "even though it also has", "way too many characters in it"] # fmt: skip
l3 = ["I have", "trailing comma", "so I should be braked",]

# output

l1 = [
"This list should be broken up",
"into multiple lines",
"because it is way too long",
]
l2 = ["But this list shouldn't", "even though it also has", "way too many characters in it"] # fmt: skip
l3 = [
"I have",
"trailing comma",
"so I should be braked",
]
20 changes: 20 additions & 0 deletions tests/data/fmtskip3.py
@@ -0,0 +1,20 @@
a = 3
# fmt: off
b, c = 1, 2
d = 6 # fmt: skip
e = 5
# fmt: on
f = ["This is a very long line that should be formatted into a clearer line ", "by rearranging."]

# output

a = 3
# fmt: off
b, c = 1, 2
d = 6 # fmt: skip
e = 5
# fmt: on
f = [
"This is a very long line that should be formatted into a clearer line ",
"by rearranging.",
]
13 changes: 13 additions & 0 deletions tests/data/fmtskip4.py
@@ -0,0 +1,13 @@
a = 2
# fmt: skip
l = [1, 2, 3,]

# output

a = 2
# fmt: skip
l = [
1,
2,
3,
]
22 changes: 22 additions & 0 deletions tests/data/fmtskip5.py
@@ -0,0 +1,22 @@
a, b, c = 3, 4, 5
if (
a == 3
and b != 9 # fmt: skip
and c is not None
):
print("I'm good!")
else:
print("I'm bad")


# output

a, b, c = 3, 4, 5
if (
a == 3
and b != 9 # fmt: skip
and c is not None
):
print("I'm good!")
else:
print("I'm bad")
5 changes: 5 additions & 0 deletions tests/test_format.py
Expand Up @@ -37,6 +37,11 @@
"fmtonoff2",
"fmtonoff3",
"fmtonoff4",
"fmtskip",
"fmtskip2",
"fmtskip3",
"fmtskip4",
"fmtskip5",
"fstring",
"function",
"function2",
Expand Down

0 comments on commit 6a105e0

Please sign in to comment.