From 6a105e019fcaa10cc0b6fb59edea4a4018eac97e Mon Sep 17 00:00:00 2001 From: Sagi Shadur Date: Mon, 15 Feb 2021 18:02:48 +0200 Subject: [PATCH 1/1] Add "# fmt: skip" directive to black (#1800) Fixes #1162 --- README.md | 5 +- src/black/__init__.py | 104 +++++++++++++++++++++++++---------------- tests/data/fmtskip.py | 3 ++ tests/data/fmtskip2.py | 17 +++++++ tests/data/fmtskip3.py | 20 ++++++++ tests/data/fmtskip4.py | 13 ++++++ tests/data/fmtskip5.py | 22 +++++++++ tests/test_format.py | 5 ++ 8 files changed, 147 insertions(+), 42 deletions(-) create mode 100644 tests/data/fmtskip.py create mode 100644 tests/data/fmtskip2.py create mode 100644 tests/data/fmtskip3.py create mode 100644 tests/data/fmtskip4.py create mode 100644 tests/data/fmtskip5.py diff --git a/README.md b/README.md index 97df938..89ef427 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/black/__init__.py b/src/black/__init__.py index 6dbb765..08930d1 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -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"} @@ -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 diff --git a/tests/data/fmtskip.py b/tests/data/fmtskip.py new file mode 100644 index 0000000..1d5836f --- /dev/null +++ b/tests/data/fmtskip.py @@ -0,0 +1,3 @@ +a, b = 1, 2 +c = 6 # fmt: skip +d = 5 diff --git a/tests/data/fmtskip2.py b/tests/data/fmtskip2.py new file mode 100644 index 0000000..e624811 --- /dev/null +++ b/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", +] \ No newline at end of file diff --git a/tests/data/fmtskip3.py b/tests/data/fmtskip3.py new file mode 100644 index 0000000..6e16688 --- /dev/null +++ b/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.", +] diff --git a/tests/data/fmtskip4.py b/tests/data/fmtskip4.py new file mode 100644 index 0000000..aadd77d --- /dev/null +++ b/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, +] \ No newline at end of file diff --git a/tests/data/fmtskip5.py b/tests/data/fmtskip5.py new file mode 100644 index 0000000..d7b15e0 --- /dev/null +++ b/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") diff --git a/tests/test_format.py b/tests/test_format.py index e467770..e0cb0b7 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -37,6 +37,11 @@ SIMPLE_CASES = [ "fmtonoff2", "fmtonoff3", "fmtonoff4", + "fmtskip", + "fmtskip2", + "fmtskip3", + "fmtskip4", + "fmtskip5", "fstring", "function", "function2", -- 2.39.5