Repository: psf/black

Issue:
Black adds an extra blank line to an import line that ends with `# fmt: skip`
<!--
Please make sure that the bug is not already fixed either in newer versions or the
current development version. To confirm this, you have three options:

1. Update Black's version if a newer release exists: `pip install -U black`
2. Use the online formatter at <https://black.vercel.app/?version=main>, which will use
   the latest main branch.
3. Or run _Black_ on your machine:
   - create a new virtualenv (make sure it's the same Python version);
   - clone this repository;
   - run `pip install -e .[d]`;
   - run `pip install -r test_requirements.txt`
   - make sure it's sane by running `python -m pytest`; and
   - run `black` like you did last time.
-->

**Describe the bug**

It reformats:

```python
import os
import re  # fmt: skip
import sys
```

to:

```python
import os

import re  # fmt: skip
import sys
```

Example: https://black.vercel.app/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4ACSAFtdAD2IimZxl1N_WlwxRhR7MSgE-ANVmAcXiiHH_vLVBv6H9oJYdLgh3uJSbWM1qliEV5h8ji5l4G2iqd5AHPniBCkwXU5Gi97RWyWgW8b3Yvc90k3x9wXSWeryKEAAAIvWais0zjPAAAF3kwEAAAAB02j7scRn-wIAAAAABFla

**Expected behavior**

`# fmt: skip` shouldn't make it add an extra line.

**Additional context**

The issue is Black internally converts the `import re  # fmt: skip` line to a `STANDALONE_COMMENT`, and for other "real" `STANDALONE_COMMENT` lines it will add the blank line as designed. Maybe internally it needs to distinguish between real `STANDALONE_COMMENT` nodes v.s. converted from fmt off/skip?


Discussion:
I didn't know we added a blank before a comment preceding imports, that's a bit surprising. Anyway, distinguishing between real and internal STANDALONE_COMMENTS sounds like a reasonable fix, but I haven't looked at the code. This also affects `fmt: off/on` FWIW:

```diff
--- test.py	2022-12-14 04:17:08.331940 +0000
+++ test.py	2022-12-14 04:17:22.664904 +0000
@@ -1,5 +1,6 @@
 import os
+
 # fmt: off
 import re
 # fmt: on
 import sys
```
 
I have also come across this bug in the situation where I have a long import statement which black reformats across multiple lines. I tried to skip formatting on this import statement but instead an extra line is created. This causes problems with tools like `isort` which then tries to remove the new blank line.

For example:
```python
from my_package.my_module import function
from my_package.my_module_with_a_very_very_long_name import function_with_a_very_very_long_name  # fmt: skip
```
Reformatted to:
```python
from my_package.my_module import function

from my_package.my_module_with_a_very_very_long_name import function_with_a_very_very_long_name  # fmt: skip
```
---
I'm not sure if it helps solve/understand the bug better, but I also noticed that if I add `# fmt: skip` to all imports in the 'block', then the extra line is not created.

For example, the following:

```python
import os
import sys  # fmt: skip
import re  # fmt: skip

import os  # fmt: skip
import sys  # fmt: skip
import re  # fmt: skip
```

Is reformatted to:
```python
import os

import sys  # fmt: skip
import re  # fmt: skip

import os  # fmt: skip
import sys  # fmt: skip
import re  # fmt: skip
```

[Playground example](https://black.vercel.app/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4ADqAGZdAD2IimZxl1N_WlwxRhR7MSgE-ANVmAcXlhY5wVPd8fTq0SI5NSKrPazpbkkjOFqRLd2jNz31MhnIZb_oh5XNQeruZEhBtrUgNW3EwP8HEvzTpM-3Gl61LY93s3_fhLH_SsyFlqcBAAAAAItGvOCquZQ0AAGCAesBAACw5PGtscRn-wIAAAAABFla)

Resolving patch:
diff --git a/src/black/comments.py b/src/black/comments.py
--- a/src/black/comments.py
+++ b/src/black/comments.py
@@ -203,6 +203,7 @@ def convert_one_fmt_off_pair(node: Node) -> bool:
                     STANDALONE_COMMENT,
                     hidden_value,
                     prefix=standalone_comment_prefix,
+                    fmt_pass_converted_first_leaf=first_leaf_of(first),
                 ),
             )
             return True
diff --git a/src/black/lines.py b/src/black/lines.py
--- a/src/black/lines.py
+++ b/src/black/lines.py
@@ -195,6 +195,26 @@ def opens_block(self) -> bool:
             return False
         return self.leaves[-1].type == token.COLON
 
+    def is_fmt_pass_converted(
+        self, *, first_leaf_matches: Optional[Callable[[Leaf], bool]] = None
+    ) -> bool:
+        """Is this line converted from fmt off/skip code?
+
+        If first_leaf_matches is not None, it only returns True if the first
+        leaf of converted code matches.
+        """
+        if len(self.leaves) != 1:
+            return False
+        leaf = self.leaves[0]
+        if (
+            leaf.type != STANDALONE_COMMENT
+            or leaf.fmt_pass_converted_first_leaf is None
+        ):
+            return False
+        return first_leaf_matches is None or first_leaf_matches(
+            leaf.fmt_pass_converted_first_leaf
+        )
+
     def contains_standalone_comments(self, depth_limit: int = sys.maxsize) -> bool:
         """If so, needs to be split before emitting."""
         for leaf in self.leaves:
@@ -597,6 +617,7 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]:
             self.previous_line
             and self.previous_line.is_import
             and not current_line.is_import
+            and not current_line.is_fmt_pass_converted(first_leaf_matches=is_import)
             and depth == self.previous_line.depth
         ):
             return (before or 1), 0
diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py
--- a/src/blib2to3/pytree.py
+++ b/src/blib2to3/pytree.py
@@ -392,6 +392,10 @@ class Leaf(Base):
     _prefix = ""  # Whitespace and comments preceding this token in the input
     lineno: int = 0  # Line where this token starts in the input
     column: int = 0  # Column where this token starts in the input
+    # If not None, this Leaf is created by converting a block of fmt off/skip
+    # code, and `fmt_pass_converted_first_leaf` points to the first Leaf in the
+    # converted code.
+    fmt_pass_converted_first_leaf: Optional["Leaf"] = None
 
     def __init__(
         self,
@@ -401,6 +405,7 @@ def __init__(
         prefix: Optional[Text] = None,
         fixers_applied: List[Any] = [],
         opening_bracket: Optional["Leaf"] = None,
+        fmt_pass_converted_first_leaf: Optional["Leaf"] = None,
     ) -> None:
         """
         Initializer.
@@ -419,6 +424,7 @@ def __init__(
         self.fixers_applied: Optional[List[Any]] = fixers_applied[:]
         self.children = []
         self.opening_bracket = opening_bracket
+        self.fmt_pass_converted_first_leaf = fmt_pass_converted_first_leaf
 
     def __repr__(self) -> str:
         """Return a canonical string representation."""


Test patch:
diff --git a/tests/data/simple_cases/fmtonoff.py b/tests/data/simple_cases/fmtonoff.py
--- a/tests/data/simple_cases/fmtonoff.py
+++ b/tests/data/simple_cases/fmtonoff.py
@@ -195,7 +195,6 @@ def single_literal_yapf_disable():
 from third_party import X, Y, Z
 
 from library import some_connection, some_decorator
-
 # fmt: off
 from third_party import (X,
                          Y, Z)
diff --git a/tests/data/simple_cases/fmtpass_imports.py b/tests/data/simple_cases/fmtpass_imports.py
new file mode 100644
--- /dev/null
+++ b/tests/data/simple_cases/fmtpass_imports.py
@@ -0,0 +1,19 @@
+# Regression test for https://github.com/psf/black/issues/3438
+
+import ast
+import collections  # fmt: skip
+import dataclasses
+# fmt: off
+import os
+# fmt: on
+import pathlib
+
+import re  # fmt: skip
+import secrets
+
+# fmt: off
+import sys
+# fmt: on
+
+import tempfile
+import zoneinfo