Repository: psf/black

Issue:
Preserve blank line before raw docstrings
**Describe the style change**

Currently, black preserves the blank line before regular doc strings, but not before raw docstrings. It should preserve them before both.

**Input**

```python
class TextIOWrapper(TextIOBase):
    
    r"""Character and line based layer over a BufferedIOBase object, buffer.

    [...]
    """

    _CHUNK_SIZE = 2048
    
class TextIOWrapper(TextIOBase):
    
    """Character and line based layer over a BufferedIOBase object, buffer.

    [...]
    """

    _CHUNK_SIZE = 2048
```

**Examples in the current _Black_ style**

```python
class TextIOWrapper(TextIOBase):
    r"""Character and line based layer over a BufferedIOBase object, buffer.

    [...]
    """

    _CHUNK_SIZE = 2048


class TextIOWrapper(TextIOBase):

    """Character and line based layer over a BufferedIOBase object, buffer.

    [...]
    """

    _CHUNK_SIZE = 2048
```

**Desired style**

```python
class TextIOWrapper(TextIOBase):

    r"""Character and line based layer over a BufferedIOBase object, buffer.

    [...]
    """

    _CHUNK_SIZE = 2048


class TextIOWrapper(TextIOBase):

    """Character and line based layer over a BufferedIOBase object, buffer.

    [...]
    """

    _CHUNK_SIZE = 2048
```

**Additional context**

Currently, black doesn't seem to consider raw triple quotes strings as docstrings, even though cpython (and PEP 257) do. I've seen some raw triple quoted doc string uses in the wild, such as the above example from cpython.


Discussion:
We should actually remove both blank lines: we do that in #3692 (currently only in the preview style). Agree that we should treat raw strings like docstrings too.

Resolving patch:
diff --git a/src/black/lines.py b/src/black/lines.py
--- a/src/black/lines.py
+++ b/src/black/lines.py
@@ -193,11 +193,16 @@ def is_class_paren_empty(self) -> bool:
     @property
     def is_triple_quoted_string(self) -> bool:
         """Is the line a triple quoted string?"""
-        return (
-            bool(self)
-            and self.leaves[0].type == token.STRING
-            and self.leaves[0].value.startswith(('"""', "'''"))
-        )
+        if not self or self.leaves[0].type != token.STRING:
+            return False
+        value = self.leaves[0].value
+        if value.startswith(('"""', "'''")):
+            return True
+        if Preview.accept_raw_docstrings in self.mode and value.startswith(
+            ("r'''", 'r"""', "R'''", 'R"""')
+        ):
+            return True
+        return False
 
     @property
     def opens_block(self) -> bool:
diff --git a/src/black/mode.py b/src/black/mode.py
--- a/src/black/mode.py
+++ b/src/black/mode.py
@@ -188,6 +188,7 @@ class Preview(Enum):
     dummy_implementations = auto()
     walrus_subscript = auto()
     module_docstring_newlines = auto()
+    accept_raw_docstrings = auto()
     fix_power_op_line_length = auto()
 
 


Test patch:
diff --git a/tests/data/raw_docstring.py b/tests/data/raw_docstring.py
new file mode 100644
--- /dev/null
+++ b/tests/data/raw_docstring.py
@@ -0,0 +1,32 @@
+# flags: --preview --skip-string-normalization
+class C:
+
+    r"""Raw"""
+
+def f():
+
+    r"""Raw"""
+
+class SingleQuotes:
+
+
+    r'''Raw'''
+
+class UpperCaseR:
+    R"""Raw"""
+
+# output
+class C:
+    r"""Raw"""
+
+
+def f():
+    r"""Raw"""
+
+
+class SingleQuotes:
+    r'''Raw'''
+
+
+class UpperCaseR:
+    R"""Raw"""