Repository: psf/black

Issue:
INTERNAL ERROR: Black produced code that is not equivalent to the source
INTERNAL ERROR: Black produced code that is not equivalent to the source

**To Reproduce** Steps to reproduce the behavior:

1. Take this file https://github.com/beaker-project/beaker/blob/8fc7edc07aeb6e545d4c80e5649601edad27b14b/Server/bkr/server/mail.py
2. Run _Black_ on it.
3. INTERNAL ERROR: Black produced code that is not equivalent to the source

**Expected behavior** A clear and concise description of what you expected to happen.
It works

**Environment (please complete the following information):**

- Version: version 20.8b1
- OS and Python version: Linux/Python Python 3.8.7

Diff from error

```
--- src
+++ dst
@@ -6617,11 +6617,11 @@
       body=
         Expr(
           value=
             Constant(
               value=
-                b' Send a system reservation notification to \n    job owner, and to job CC list if any. ',  # bytes
+                b'Send a system reservation notification to\n    job owner, and to job CC list if any.',  # bytes
             )  # /Constant
         )  # /Expr
         Assign(
           targets=
             Name(

```


Discussion:

Resolving patch:
diff --git a/src/black/__init__.py b/src/black/__init__.py
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -6466,12 +6466,22 @@ def _stringify_ast(
             # Constant strings may be indented across newlines, if they are
             # docstrings; fold spaces after newlines when comparing. Similarly,
             # trailing and leading space may be removed.
+            # Note that when formatting Python 2 code, at least with Windows
+            # line-endings, docstrings can end up here as bytes instead of
+            # str so make sure that we handle both cases.
             if (
                 isinstance(node, ast.Constant)
                 and field == "value"
-                and isinstance(value, str)
+                and isinstance(value, (str, bytes))
             ):
-                normalized = re.sub(r" *\n[ \t]*", "\n", value).strip()
+                lineend = "\n" if isinstance(value, str) else b"\n"
+                # To normalize, we strip any leading and trailing space from
+                # each line...
+                stripped = [line.strip() for line in value.splitlines()]
+                normalized = lineend.join(stripped)  # type: ignore[attr-defined]
+                # ...and remove any blank lines at the beginning and end of
+                # the whole string
+                normalized = normalized.strip()
             else:
                 normalized = value
             yield f"{'  ' * (depth+2)}{normalized!r},  # {value.__class__.__name__}"


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -1918,6 +1918,26 @@ def test_bpo_2142_workaround(self) -> None:
         actual = diff_header.sub(DETERMINISTIC_HEADER, actual)
         self.assertEqual(actual, expected)
 
+    def test_docstring_reformat_for_py27(self) -> None:
+        """
+        Check that stripping trailing whitespace from Python 2 docstrings
+        doesn't trigger a "not equivalent to source" error
+        """
+        source = (
+            b'def foo():\r\n    """Testing\r\n    Testing """\r\n    print "Foo"\r\n'
+        )
+        expected = 'def foo():\n    """Testing\n    Testing"""\n    print "Foo"\n'
+
+        result = CliRunner().invoke(
+            black.main,
+            ["-", "-q", "--target-version=py27"],
+            input=BytesIO(source),
+        )
+
+        self.assertEqual(result.exit_code, 0)
+        actual = result.output
+        self.assertFormatEqual(actual, expected)
+
 
 with open(black.__file__, "r", encoding="utf-8") as _bf:
     black_source_lines = _bf.readlines()