Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "# fmt: skip" directive to black #1800

Merged
merged 8 commits into from Feb 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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