Repository: psf/black

Issue:
Whitespace before `fmt: skip` comment is normalised
**Describe the bug**

Apologies if this is a duplicate; I have read:

- https://github.com/psf/black/issues/3330
- https://github.com/psf/black/pull/3959/files
- https://github.com/psf/black/issues/2213

Based on https://github.com/psf/black/pull/3959, Black should support e.g. this: `# pylint: disable=redefined-outer-name # fmt: skip` and yet I cannot get it to work on the newest Black version.

I have the following line:

```
client = billingdataservice.app.test_client() # pylint: disable=redefined-outer-name
```

The line is too long for black, so when I run black, I get this:

```
client = (
    billingdataservice.app.test_client()
)  # pylint: disable=redefined-outer-name
```

We don't want that, so I tried to use `fmt: skip`. I tried the following comments:

- `client = billingdataservice.app.test_client() # pylint: disable=redefined-outer-name; fmt: skip`
- `client = billingdataservice.app.test_client() # pylint: disable=redefined-outer-name # fmt: skip`
- `client = billingdataservice.app.test_client() # fmt: skip # pylint: disable=redefined-outer-name`
- `client = billingdataservice.app.test_client() # fmt: skip; pylint: disable=redefined-outer-name`

as well as mixing number of spaces before/after the hash sign, e.g. `...test_client()  #  fmt: skip; pylint: ...` or `...test_client()  # fmt: skip; pylint: ...` etc.

**Reproducer**

`file.py`:

```
def client():
    client = billingdataservice.app.test_client() # fmt: skip; pylint: disable=redefined-outer-name
```

Command:

```sh
$ black --preview file.py
```

(The `--preview` flag seems not to matter for this issue; behavior is identical with or without it)

Black always modifies the line, e.g.:

```
def client():
    client = ( 
        billingdataservice.app.test_client()
    )  # fmt: skip; pylint: disable=redefined-outer-name
```

Which means `fmt: skip` did not get honored.

**Environment**

<!-- Please complete the following information: -->

```
$ black --version
black, 23.12.1 (compiled: yes)
Python (CPython) 3.11.6
```

- Black's version: 23.12.1
- OS and Python version: OpenSUSE Tumbleweed, Python 3.11



Discussion:
In fact, the `# fmt: skip` comment seems not to be honored at all?

For example, black still modifies `client = billingdataservice.app.test_client() # fmt: skip`  -> `client = billingdataservice.app.test_client()  # fmt: skip` (note added space before the `#` character)

Is that expected behavior? 
Simplest reproducer:

```
$ black -c "test # fmt:skip"
test  # fmt:skip
$ black -c "test # fmt: skip"
test  # fmt: skip
$ black --preview -c "test # fmt: skip"
test  # fmt: skip
$ black --preview -c "test # fmt:skip"
test  # fmt:skip
```

Despite a line that contains `fmt: skip` comment, that line is modified by Black (always added a space before the `#` symbol). Not sure if I'm missing some flag or configuration.
I can't reproduce much of what you're reporting. You may be running a different version of Black than you think you're running.

```
% black -c "1+1 # fmt:skip; some other comment"          
1+1  # fmt:skip; some other comment
% black -c '''def client():
    client = billingdataservice.app.test_client() # fmt: skip; pylint: disable=redefined-outer-name
'''
def client():
    client = billingdataservice.app.test_client()  # fmt: skip; pylint: disable=redefined-outer-name
% black --version                                       
black, 23.12.1 (compiled: yes)
Python (CPython) 3.11.1
```
Hmm, this is strange. The above output is from Black I have installed by using `pip install black`.

I have reproduced the issue on the container we have in the repo, which I locally built with the `black-container` name:

```
podman run --rm black-container:latest black -c "test # fmt:skip"
test  # fmt:skip
$ podman run --rm black-container:latest black -c '''def client():
>     client = billingdataservice.app.test_client() # fmt: skip; pylint: disable=redefined-outer-name
> '''
def client():
    client = (
        billingdataservice.app.test_client()
    )  # fmt: skip; pylint: disable=redefined-outer-name
```

Similarly, I can do the same on Fedora (a different computer) when installing Black directly from Github:

```
$ black -c '''def client():
    client = billingdataservice.app.test_client() # fmt: skip; pylint: disable=redefined-outer-name
'''
def client():
    client = (
        billingdataservice.app.test_client()
    )  # fmt: skip; pylint: disable=redefined-outer-name

$ black --version
black, 23.12.2.dev13+ge11eaf2 (compiled: no)
Python (CPython) 3.12.1
```
I am a bit stumped as to what could be the discrepancy here... 
@m-czernek the fix is only in the preview style (although I think technically it would have met the stability policy).

That is, your examples are fixed if you use `--preview`. I bet @JelleZijlstra is just working in a codebase where he has preview style in his config.

(We're also at the time of year where we cut preview style over to stable, so follow on at https://github.com/psf/black/issues/4042 if you're interested)
@hauntsaninja I'm not sure you understand the issue. The examples seems _not_ to be fixed by using `--preview` flag, for example using the container on your main:

```
podman run --rm black-container:latest black --preview -c "test # fmt:skip"
test  # fmt:skip
```

In other words, the `# fmt:skip` functionality seems to me not to work regardless of the `--preview` flag.
Okay, apologies, I was looking too much at https://github.com/psf/black/issues/4143#issuecomment-1875784656 / mention of https://github.com/psf/black/pull/3959

IIUC the only issue remaining in preview is that Black normalises the whitespace before `# fmt: skip` (it does this on stable as well). This is regardless of whether there are other comments present on the line or not.

Does that sound right to you? (I've renamed the issue, but will rename again if there's something else as well)
See also https://github.com/psf/black/pull/2970#pullrequestreview-932691073

Resolving patch:
diff --git a/src/black/comments.py b/src/black/comments.py
--- a/src/black/comments.py
+++ b/src/black/comments.py
@@ -3,7 +3,7 @@
 from functools import lru_cache
 from typing import Collection, Final, Iterator, List, Optional, Tuple, Union
 
-from black.mode import Mode
+from black.mode import Mode, Preview
 from black.nodes import (
     CLOSING_BRACKETS,
     STANDALONE_COMMENT,
@@ -46,6 +46,7 @@ class ProtoComment:
     newlines: int  # how many newlines before the comment
     consumed: int  # how many characters of the original leaf's prefix did we consume
     form_feed: bool  # is there a form feed before the comment
+    leading_whitespace: str  # leading whitespace before the comment, if any
 
 
 def generate_comments(leaf: LN) -> Iterator[Leaf]:
@@ -88,7 +89,9 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]:
     form_feed = False
     for index, full_line in enumerate(re.split("\r?\n", prefix)):
         consumed += len(full_line) + 1  # adding the length of the split '\n'
-        line = full_line.lstrip()
+        match = re.match(r"^(\s*)(\S.*|)$", full_line)
+        assert match
+        whitespace, line = match.groups()
         if not line:
             nlines += 1
             if "\f" in full_line:
@@ -113,6 +116,7 @@ def list_comments(prefix: str, *, is_endmarker: bool) -> List[ProtoComment]:
                 newlines=nlines,
                 consumed=consumed,
                 form_feed=form_feed,
+                leading_whitespace=whitespace,
             )
         )
         form_feed = False
@@ -230,7 +234,11 @@ def convert_one_fmt_off_pair(
                 standalone_comment_prefix += fmt_off_prefix
                 hidden_value = comment.value + "\n" + hidden_value
             if _contains_fmt_skip_comment(comment.value, mode):
-                hidden_value += "  " + comment.value
+                hidden_value += (
+                    comment.leading_whitespace
+                    if Preview.no_normalize_fmt_skip_whitespace in mode
+                    else "  "
+                ) + 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).
diff --git a/src/black/mode.py b/src/black/mode.py
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -174,6 +174,7 @@ class Preview(Enum):
     string_processing = auto()
     hug_parens_with_braces_and_square_brackets = auto()
     unify_docstring_detection = auto()
+    no_normalize_fmt_skip_whitespace = auto()
     wrap_long_dict_values_in_parens = auto()
     multiline_string_handling = auto()
 


Test patch:
diff --git a/tests/data/cases/fmtskip9.py b/tests/data/cases/fmtskip9.py
new file mode 100644
--- /dev/null
+++ b/tests/data/cases/fmtskip9.py
@@ -0,0 +1,9 @@
+# flags: --preview
+print () # fmt: skip
+print () # fmt:skip
+
+
+# output
+
+print () # fmt: skip
+print () # fmt:skip