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

This commit is contained in:
Henri Holopainen 2023-10-25 19:47:21 +03:00 committed by GitHub
parent 1d4c31aa58
commit 878937bcc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 19 deletions

View File

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

View File

@ -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 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. 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: 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 `# fmt: skip` can be mixed with other pragmas/comments either with multiple comments
no unindents beyond the initial indentation level between them. It also recognizes (e.g. `# fmt: skip # pylint # noqa`) or as a semicolon separeted list (e.g.
[YAPF](https://github.com/google/yapf)'s block comments to the same effect, as a `# fmt: skip; pylint; noqa`). `# fmt: on/off` must be on the same level of indentation
courtesy for straddling code. 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 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 in trying out where the style is heading, see [future style](./future_style.md) and try

View File

@ -1099,7 +1099,7 @@ def _format_str_once(src_contents: str, *, mode: Mode) -> str:
for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS} for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS}
if supports_feature(versions, feature) if supports_feature(versions, feature)
} }
normalize_fmt_off(src_node) normalize_fmt_off(src_node, mode)
lines = LineGenerator(mode=mode, features=context_manager_features) lines = LineGenerator(mode=mode, features=context_manager_features)
elt = EmptyLineTracker(mode=mode) elt = EmptyLineTracker(mode=mode)
split_line_features = { split_line_features = {

View File

@ -3,6 +3,7 @@
from functools import lru_cache from functools import lru_cache
from typing import Final, Iterator, List, Optional, Union from typing import Final, Iterator, List, Optional, Union
from black.mode import Mode, Preview
from black.nodes import ( from black.nodes import (
CLOSING_BRACKETS, CLOSING_BRACKETS,
STANDALONE_COMMENT, STANDALONE_COMMENT,
@ -20,10 +21,11 @@
FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"} FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"}
FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"} FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"}
FMT_PASS: Final = {*FMT_OFF, *FMT_SKIP}
FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"} FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"}
COMMENT_EXCEPTIONS = " !:#'" COMMENT_EXCEPTIONS = " !:#'"
_COMMENT_PREFIX = "# "
_COMMENT_LIST_SEPARATOR = ";"
@dataclass @dataclass
@ -130,14 +132,14 @@ def make_comment(content: str) -> str:
return "#" + content 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.""" """Convert content between `# fmt: off`/`# fmt: on` into standalone comments."""
try_again = True try_again = True
while try_again: 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. """Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment.
Returns True if a pair was converted. Returns True if a pair was converted.
@ -145,21 +147,27 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
for leaf in node.leaves(): for leaf in node.leaves():
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 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 previous_consumed = comment.consumed
continue continue
# We only want standalone comments. If there's no previous leaf or # We only want standalone comments. If there's no previous leaf or
# the previous leaf is indentation, it's a standalone comment in # the previous leaf is indentation, it's a standalone comment in
# disguise. # 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) prev = preceding_leaf(leaf)
if prev: if prev:
if comment.value in FMT_OFF and prev.type not in WHITESPACE: if comment.value in FMT_OFF and prev.type not in WHITESPACE:
continue 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 continue
ignored_nodes = list(generate_ignored_nodes(leaf, comment)) ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode))
if not ignored_nodes: if not ignored_nodes:
continue continue
@ -168,7 +176,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
prefix = first.prefix prefix = first.prefix
if comment.value in FMT_OFF: if comment.value in FMT_OFF:
first.prefix = prefix[comment.consumed :] first.prefix = prefix[comment.consumed :]
if comment.value in FMT_SKIP: if _contains_fmt_skip_comment(comment.value, mode):
first.prefix = "" first.prefix = ""
standalone_comment_prefix = prefix standalone_comment_prefix = prefix
else: else:
@ -178,7 +186,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
hidden_value = "".join(str(n) for n in ignored_nodes) hidden_value = "".join(str(n) for n in ignored_nodes)
if comment.value in FMT_OFF: if comment.value in FMT_OFF:
hidden_value = comment.value + "\n" + hidden_value 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 hidden_value += " " + comment.value
if hidden_value.endswith("\n"): if hidden_value.endswith("\n"):
# That happens when one of the `ignored_nodes` ended with a NEWLINE # That happens when one of the `ignored_nodes` ended with a NEWLINE
@ -205,13 +213,15 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
return False 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`. """Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
If comment is skip, returns leaf only. If comment is skip, returns leaf only.
Stops at the end of the block. 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) yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment)
return return
container: Optional[LN] = container_of(leaf) container: Optional[LN] = container_of(leaf)
@ -327,3 +337,32 @@ def contains_pragma_comment(comment_list: List[Leaf]) -> bool:
return True return True
return False 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)

View File

@ -192,6 +192,7 @@ class Preview(Enum):
fix_power_op_line_length = auto() fix_power_op_line_length = auto()
hug_parens_with_braces_and_square_brackets = auto() hug_parens_with_braces_and_square_brackets = auto()
allow_empty_first_line_before_new_block_or_comment = auto() allow_empty_first_line_before_new_block_or_comment = auto()
single_line_format_skip_with_multiple_comments = auto()
class Deprecated(UserWarning): class Deprecated(UserWarning):

View File

@ -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