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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2213] Add support for single line format skip with other comments on the same line #3959

Merged
merged 13 commits into from Oct 25, 2023
2 changes: 1 addition & 1 deletion CHANGES.md
Expand Up @@ -17,7 +17,7 @@

### Configuration

<!-- Changes to how Black can be configured -->
- Add support for single line format skip with other comments on the same line (#3959)

### Packaging

Expand Down
12 changes: 7 additions & 5 deletions docs/the_black_code_style/current_style.md
Expand Up @@ -8,12 +8,14 @@ deliberately limited and rarely added. Previous formatting is taken into account
little as possible, with rare exceptions like the magic trailing comma. The coding style
used by _Black_ can be viewed as a strict subset of PEP 8.

_Black_ reformats entire files in place. It doesn't reformat lines that end with
_Black_ reformats entire files in place. It doesn't reformat lines that contain
`# fmt: skip` or blocks that start with `# fmt: off` and end with `# fmt: on`.
`# fmt: on/off` must be on the same level of indentation and in the same block, meaning
no unindents beyond the initial indentation level between them. It also recognizes
[YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a
courtesy for straddling code.
`# fmt: skip` can be mixed with other pragmas/comments either with multiple comments
(e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separeted list (e.g.
`# fmt: skip; pylint; noqa`). `# fmt: on/off` must be on the same level of indentation
henriholopainen marked this conversation as resolved.
Show resolved Hide resolved
and in the same block, meaning no unindents beyond the initial indentation level between
them. It also recognizes [YAPF](https://github.com/google/yapf)'s block comments to the
same effect, as a courtesy for straddling code.

The rest of this document describes the current formatting style. If you're interested
in trying out where the style is heading, see [future style](./future_style.md) and try
Expand Down
2 changes: 1 addition & 1 deletion src/black/__init__.py
Expand Up @@ -1099,7 +1099,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str:
for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS}
if supports_feature(versions, feature)
}
normalize_fmt_off(src_node)
normalize_fmt_off(src_node, mode)
lines = LineGenerator(mode=mode, features=context_manager_features)
elt = EmptyLineTracker(mode=mode)
split_line_features = {
Expand Down
63 changes: 51 additions & 12 deletions src/black/comments.py
Expand Up @@ -3,6 +3,7 @@
from functools import lru_cache
from typing import Final, Iterator, List, Optional, Union

from black.mode import Mode, Preview
from black.nodes import (
CLOSING_BRACKETS,
STANDALONE_COMMENT,
Expand All @@ -20,10 +21,11 @@

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

COMMENT_EXCEPTIONS = " !:#'"
_COMMENT_PREFIX = "# "
_COMMENT_LIST_SEPARATOR = ";"


@dataclass
Expand Down Expand Up @@ -130,36 +132,42 @@ def make_comment(content: str) -> str:
return "#" + content


def normalize_fmt_off(node: Node) -> None:
def normalize_fmt_off(node: Node, mode: Mode) -> None:
"""Convert content between `# fmt: off`/`# fmt: on` into standalone comments."""
try_again = True
while try_again:
try_again = convert_one_fmt_off_pair(node)
try_again = convert_one_fmt_off_pair(node, mode)


def convert_one_fmt_off_pair(node: Node) -> bool:
def convert_one_fmt_off_pair(node: Node, mode: Mode) -> bool:
"""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():
previous_consumed = 0
for comment in list_comments(leaf.prefix, is_endmarker=False):
if comment.value not in FMT_PASS:
should_pass_fmt = comment.value in FMT_OFF or _contains_fmt_skip_comment(
comment.value, mode
)
if not should_pass_fmt:
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:
if should_pass_fmt 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:
if (
_contains_fmt_skip_comment(comment.value, mode)
and prev.type in WHITESPACE
):
continue

ignored_nodes = list(generate_ignored_nodes(leaf, comment))
ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode))
if not ignored_nodes:
continue

Expand All @@ -168,7 +176,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
prefix = first.prefix
if comment.value in FMT_OFF:
first.prefix = prefix[comment.consumed :]
if comment.value in FMT_SKIP:
if _contains_fmt_skip_comment(comment.value, mode):
first.prefix = ""
standalone_comment_prefix = prefix
else:
Expand All @@ -178,7 +186,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
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:
if _contains_fmt_skip_comment(comment.value, mode):
hidden_value += " " + comment.value
if hidden_value.endswith("\n"):
# That happens when one of the `ignored_nodes` ended with a NEWLINE
Expand All @@ -205,13 +213,15 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
return False


def generate_ignored_nodes(leaf: Leaf, comment: ProtoComment) -> Iterator[LN]:
def generate_ignored_nodes(
leaf: Leaf, comment: ProtoComment, mode: Mode
) -> 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.
"""
if comment.value in FMT_SKIP:
if _contains_fmt_skip_comment(comment.value, mode):
yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment)
return
container: Optional[LN] = container_of(leaf)
Expand Down Expand Up @@ -327,3 +337,32 @@ def contains_pragma_comment(comment_list: List[Leaf]) -> bool:
return True

return False


def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool:
"""
Checks if the given comment contains FMT_SKIP alone or paired with other comments.
Matching styles:
# fmt:skip <-- single comment
# noqa:XXX # fmt:skip # a nice line <-- multiple comments (Preview)
# pylint:XXX; fmt:skip <-- list of comments (; separated, Preview)
"""
semantic_comment_blocks = (
[
comment_line,
*[
_COMMENT_PREFIX + comment.strip()
for comment in comment_line.split(_COMMENT_PREFIX)[1:]
],
*[
_COMMENT_PREFIX + comment.strip()
for comment in comment_line.strip(_COMMENT_PREFIX).split(
_COMMENT_LIST_SEPARATOR
)
],
]
if Preview.single_line_format_skip_with_multiple_comments in mode
else [comment_line]
)

return any(comment in FMT_SKIP for comment in semantic_comment_blocks)
1 change: 1 addition & 0 deletions src/black/mode.py
Expand Up @@ -192,6 +192,7 @@ class Preview(Enum):
fix_power_op_line_length = auto()
hug_parens_with_braces_and_square_brackets = auto()
allow_empty_first_line_before_new_block_or_comment = auto()
single_line_format_skip_with_multiple_comments = auto()


class Deprecated(UserWarning):
Expand Down
@@ -0,0 +1,20 @@
# flags: --preview
foo = 123 # fmt: skip # noqa: E501 # pylint
bar = (
123 ,
( 1 + 5 ) # pylint # fmt:skip
)
baz = "a" + "b" # pylint; fmt: skip; noqa: E501
skip_will_not_work = "a" + "b" # pylint fmt:skip
skip_will_not_work2 = "a" + "b" # some text; fmt:skip happens to be part of it

# output

foo = 123 # fmt: skip # noqa: E501 # pylint
bar = (
123 ,
( 1 + 5 ) # pylint # fmt:skip
)
baz = "a" + "b" # pylint; fmt: skip; noqa: E501
skip_will_not_work = "a" + "b" # pylint fmt:skip
skip_will_not_work2 = "a" + "b" # some text; fmt:skip happens to be part of it