Repository: psf/black

Issue:
# fmt: pass - ignore formatting for single statements
Feature request:
`# fmt: pass` would skip formatting for the single statement that immediately follows.

**Is your feature request related to a problem? Please describe.** 

Not a response to a problem but it might be helpful for the following reasons:

- Easier to use / less typing
- Can't forget to turn it back on
  - Or accidentally insert code you *do* want formatted into an unformatted block
- Encourages ignoring less code
  - One-offs are always isolated to one statement instead of potentially large chunks of code.

**Describe the solution you'd like**

```python
z = "hello " + "world"

# fmt: pass
custom_formatting = [
    0,  1,  2,
    3,  4,  5,
    6,  7,  8,
]

regular_formatting = [
   0,  1,  2,  'this will be formatted'
]
```

The `custom_formatting` list would be ignored, everything else would be formatted.

**Describe alternatives you've considered**

1. Current solution

```python
# fmt: off
ugly_code=('but I want it this way')
even_more=('dont\'t judge me')
# fmt: on
```
2. `#fmt: off` at end of line (#790)
   I believe `# fmt: pass` addresses the same concern while also being a bit more flexible.
    - a single statement could be multi-line
    - a long line would bury an inline comment
    - `# fmt: pass` is more readable & predictable as a separate line
3. Disable formatting for certain expressions only. #798
4. Deal with it

**Additional context**

Naming:

- `# fmt: pass` reflects Python's existing [pass statement](https://docs.python.org/3/reference/simple_stmts.html?highlight=pass#the-pass-statement). Hopefully this hints that you can use it to temporarily appease the formatter for one quick thing, but it's not something you want everywhere.

- Naming could be anything, e.g. "pass", "skip", "ignore"

Discussion:
ichard26: The lack of documentation additions scares me :-) I would like to see a quick explanation on how it works and a few examples. Although honestly the documentation needs to be restructured again since it's so messy right now. I don't know about my availability but I'll probably end up being the guy who will handle this refactor. See #1759.
cooperlees: Yeah my bad there. Good call @ichard26 - If you make a plan and split up how you'd like things refactored we might be able to get help from others on that so you don't have to do it all!
saroad2: Hey guys, you want me to fill the documentation gaps, or you're on it?
HarrySky: Great feature! What is the status of this PR? Friendly ping @ichard26 @cooperlees 
mrgnw: I got this error, looks like the first pass removes `# fmt: skip`.
```sh
error: cannot format blackskip.py: INTERNAL ERROR: Black produced different code on the second pass of the formatter.  Please report a bug on https://github.com/psf/black/issues.  This diff might be helpful:
```

Log:
```diff
Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,11 +1,8 @@
-z = 'hello ' + "world"
+z = "hello " + "world"
 
-# fmt: skip
 custom_formatting = [
     0,  1,
     2
 ]
 
-regular_formatting = [
-   0,  1,  2,  'this will be formatted'
-]
+regular_formatting = [0, 1, 2, "this will be formatted"]
--- first pass
+++ second pass
@@ -1,8 +1,5 @@
 z = "hello " + "world"
 
-custom_formatting = [
-    0,  1,
-    2
-]
+custom_formatting = [0, 1, 2]
 
 regular_formatting = [0, 1, 2, "this will be formatted"]
```
(Python 3.8.0 / macOS 10.15, `pip install git+git://github.com/saroad2/black.git@add_skip`)
saroad2: First of all, nice catch @mrgnw ! I fixed the bug and added a unit test to validate this won't happen again.

However, this is a misuse of the `# fmt: skip` directive. You should use this directive at the end of the line you wish to skip. For example:

```python
l = ["I", "will", "not", "be", "formatted",]  # fmt: skip
```

The following **will not work**:

```python
# fmt: skip
l = ["I", "will", "be", "formatted",] 
```
mrgnw: I see, thank you! I just copy+pasted the example from #1162. Technically I think this is more in line with #790 because it's in-line, but I think this is great and we'd all be happy to have it.
saroad2: Kindly reminding that this PR is ready to be merged.
JelleZijlstra: Could you add tests that put `# fmt: skip` in the middle of a complicated statement like a multi-line if condition? That's where we got most of our stability bugs lately, so it would be good to make sure this new feature doesn't introduce more.

I'm supportive of merging otherwise.
saroad2: Hey @JelleZijlstra , thank you for the response!
Apparently, there was actually a bug there! nice catch!

I fixed the bug and added another unit test to check the new behavior. If you have some other edge cases you think we should address, please let me know.
teresaejunior: Sorry for the +1, but since this PR is ready, it would come in very handy for me, since I'm in the process of sprinkling my code with debug lines that are to be stripped on code optimization with `python -O`, like:
```py
if __debug__: log.debug(f"{repr(big_dict)[:100]=}") # fmt: skip
```
black reformats it into two lines.
saroad2: Hey @cooperlees , @ichard26 , @JelleZijlstra, and maybe other collaborators of the Black project.

This PR is almost 4 months old. I know you are busy people, and I know that this a voluntary project, but I would be very disappointed if a new version of Black would be released without it.

So please, if something is missing in this PR, let me know and I'll fix/add/update it.
If not, let's merge this thing!

Again, I must emphasize that I don't mean to harm anyone or to be disrespectful. I just want to see my work being used :)
ichard26: TBH, I'm a tiny bit hesitant since this _may_ go against of Black's goal of being consistent, but since we have `fmt: off/on` already and while this will probably mean less consistent code since `# fmt: skip` is convenient, the benefits seem worth it. The only actual thing delaying/blocking this is that this should have documentation. I've given up on finishing up the reorganization effort before this PR gets merged since it's 99.9999999% not gonna happen due to an aforementioned lack of time. Aplogies for not getting back to you sooner. For now, I recommend adding the docs [to this section](https://github.com/psf/black/blob/master/README.md#the-black-code-style). I can deal with the messiness myself, eventually.
saroad2: Hey @ichard26 , thanks for the feedback!

I added the missing documentation piece to the README file.
As for the build failure, I have no idea what causes it, but it doesn't seem like my code.

Let me know if something else is missing!

Patch:
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


Test patch:
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 @@
     "fmtonoff2",
     "fmtonoff3",
     "fmtonoff4",
+    "fmtskip",
+    "fmtskip2",
+    "fmtskip3",
+    "fmtskip4",
+    "fmtskip5",
     "fstring",
     "function",
     "function2",