Skip to content

Commit

Permalink
[2213] Add support for single line format skip with other comments on…
Browse files Browse the repository at this point in the history
… the same line (#3959)
  • Loading branch information
henriholopainen committed Oct 25, 2023
1 parent 1d4c31a commit 878937b
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 19 deletions.
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
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

0 comments on commit 878937b

Please sign in to comment.