Repository: psf/black

Issue:
style regression: too long single line docstrings have their quotes moved to a new line.
<!--
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**

Single-line docstrings that are longer than the line length have the final three `"` moved to a new line.

<!-- A clear and concise description of what the bug is. -->

**To Reproduce**

For example, take this code:
```py
from typing import Optional

# leaves this the same
def large_image_url(self) -> Optional[str]:
    """Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.
    """
    pass


# leaves this the same too
def large_image_url(self) -> Optional[str]:
    """
    Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.
    """
    pass


# depending on the length of the docstring, this one *may* be left as-is
def large_image_url(self) -> Optional[str]:
    """
    Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable."""
    pass


# but this one is modified
def large_image_url(self) -> Optional[str]:
    """Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable."""
    pass

```

And run it with these arguments:

```sh
$ black file.py --preview
```

We get this format:

```py
from typing import Optional

# leaves this the same
def large_image_url(self) -> Optional[str]:
    """Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.
    """
    pass


# leaves this the same too
def large_image_url(self) -> Optional[str]:
    """
    Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.
    """
    pass


# this one is also left as-is
def large_image_url(self) -> Optional[str]:
    """
    Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.
    """
    pass


# but this one is modified
def large_image_url(self) -> Optional[str]:
    """Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.
    """
    pass
```

**Expected behavior**

<!-- A clear and concise description of what you expected to happen. -->

Single line docstrings are left alone. I understand what this change is supposed to do, and on already multiline docstrings this wouldn't be an issue (although it would be nice for the docstring formatter to be entirely consistent among all docstrings instead of having different behavior for the line length but i digress).

**Environment**

<!-- Please complete the following information: -->

- Black's version: <!-- e.g. [main] --> 22.10.0
- OS and Python version: <!-- e.g. [Linux/Python 3.7.4rc1] --> Python 3.11.0rc2, on linux, but i can recreate with python 3.8 too


**Additional Comments**

This seems like a regression from #3044


Discussion:
Hey! I'd like to contribute to fixing this bug as part of Hacktoberfest.
Hope that's ok!
Hi! Looking at the changes again, it seems intended.

@itxasos23 Thanks for offering! However, #2885 deals with docstrings and could clash with this change. So I think we'll at least have to discuss a bit. If you'd be willing to try something else, have a look at the ones in https://github.com/psf/black/labels/good%20first%20issue!
> Hi! Looking at the changes again, it seems intended.
> 

The biggest problem is this change for single line docstrings is a violation of [PEP 257](https://peps.python.org/pep-0257/#one-line-docstrings). 

- The closing quotes are on the same line as the opening quotes. This looks better for one-liners.

@felix-hilden Amazing! I see you got that covered on that PR :smile: 
> However, #2885 deals with docstrings and could clash with this change.

This pr is in agreement with 2885, as the author of 2885 states:

> At the moment, any docstring with only one line of content will be converted to the one-line form. 

This is in line with pep 257, and should satisfy this issue as well.

Resolving patch:
diff --git a/src/black/linegen.py b/src/black/linegen.py
--- a/src/black/linegen.py
+++ b/src/black/linegen.py
@@ -389,19 +389,18 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
                 # We need to find the length of the last line of the docstring
                 # to find if we can add the closing quotes to the line without
                 # exceeding the maximum line length.
-                # If docstring is one line, then we need to add the length
-                # of the indent, prefix, and starting quotes. Ending quotes are
-                # handled later.
+                # If docstring is one line, we don't put the closing quotes on a
+                # separate line because it looks ugly (#3320).
                 lines = docstring.splitlines()
                 last_line_length = len(lines[-1]) if docstring else 0
 
-                if len(lines) == 1:
-                    last_line_length += len(indent) + len(prefix) + quote_len
-
                 # If adding closing quotes would cause the last line to exceed
                 # the maximum line length then put a line break before the
                 # closing quotes
-                if last_line_length + quote_len > self.mode.line_length:
+                if (
+                    len(lines) > 1
+                    and last_line_length + quote_len > self.mode.line_length
+                ):
                     leaf.value = prefix + quote + docstring + "\n" + indent + quote
                 else:
                     leaf.value = prefix + quote + docstring + quote


Test patch:
diff --git a/tests/data/preview/docstring_preview.py b/tests/data/preview/docstring_preview.py
--- a/tests/data/preview/docstring_preview.py
+++ b/tests/data/preview/docstring_preview.py
@@ -54,13 +54,11 @@ def single_quote_docstring_over_line_limit2():
 
 
 def docstring_almost_at_line_limit():
-    """long docstring.................................................................
-    """
+    """long docstring................................................................."""
 
 
 def docstring_almost_at_line_limit_with_prefix():
-    f"""long docstring................................................................
-    """
+    f"""long docstring................................................................"""
 
 
 def mulitline_docstring_almost_at_line_limit():