Repository: psf/black

Issue:
(🐞) Windows line endings not preserved with blackd
<!--
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**
```
👉 blackd --bind-port 9090 &
👉 curl -s -XPOST "localhost:9090" -d "1`r`n" -o test.py
```
(NOTE: I am using \` here to demonstrate a Windows moment, feel free to replace the data with `-d 1\r\n` if you are using a shell like `fish` or something)

`test.py` contents are now `1\n` instead of the expected `1\r\n`
<!-- A clear and concise description of what the bug is. -->



**Environment**

<!-- Please complete the following information: -->

- Black's version: <!-- e.g. [main] --> 22.8.0
- OS and Python version: <!-- e.g. [Linux/Python 3.7.4rc1] -->  Windows 10 python 3.10.6

**Additional context**

<!-- Add any other context about the problem here. -->
PR adding support for this https://github.com/psf/black/pull/288



Discussion:

Resolving patch:
diff --git a/src/blackd/__init__.py b/src/blackd/__init__.py
--- a/src/blackd/__init__.py
+++ b/src/blackd/__init__.py
@@ -133,6 +133,13 @@ async def handle(request: web.Request, executor: Executor) -> web.Response:
             executor, partial(black.format_file_contents, req_str, fast=fast, mode=mode)
         )
 
+        # Preserve CRLF line endings
+        if req_str[req_str.find("\n") - 1] == "\r":
+            formatted_str = formatted_str.replace("\n", "\r\n")
+            # If, after swapping line endings, nothing changed, then say so
+            if formatted_str == req_str:
+                raise black.NothingChanged
+
         # Only output the diff in the HTTP response
         only_diff = bool(request.headers.get(DIFF_HEADER, False))
         if only_diff:


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -1286,6 +1286,17 @@ def test_preserves_line_endings_via_stdin(self) -> None:
             if nl == "\n":
                 self.assertNotIn(b"\r\n", output)
 
+    def test_normalize_line_endings(self) -> None:
+        with TemporaryDirectory() as workspace:
+            test_file = Path(workspace) / "test.py"
+            for data, expected in (
+                (b"c\r\nc\n ", b"c\r\nc\r\n"),
+                (b"l\nl\r\n ", b"l\nl\n"),
+            ):
+                test_file.write_bytes(data)
+                ff(test_file, write_back=black.WriteBack.YES)
+                self.assertEqual(test_file.read_bytes(), expected)
+
     def test_assert_equivalent_different_asts(self) -> None:
         with self.assertRaises(AssertionError):
             black.assert_equivalent("{}", "None")
diff --git a/tests/test_blackd.py b/tests/test_blackd.py
--- a/tests/test_blackd.py
+++ b/tests/test_blackd.py
@@ -209,3 +209,20 @@ async def test_cors_headers_present(self) -> None:
         response = await self.client.post("/", headers={"Origin": "*"})
         self.assertIsNotNone(response.headers.get("Access-Control-Allow-Origin"))
         self.assertIsNotNone(response.headers.get("Access-Control-Expose-Headers"))
+
+    @unittest_run_loop
+    async def test_preserves_line_endings(self) -> None:
+        for data in (b"c\r\nc\r\n", b"l\nl\n"):
+            # test preserved newlines when reformatted
+            response = await self.client.post("/", data=data + b" ")
+            self.assertEqual(await response.text(), data.decode())
+            # test 204 when no change
+            response = await self.client.post("/", data=data)
+            self.assertEqual(response.status, 204)
+
+    @unittest_run_loop
+    async def test_normalizes_line_endings(self) -> None:
+        for data, expected in ((b"c\r\nc\n", "c\r\nc\r\n"), (b"l\nl\r\n", "l\nl\n")):
+            response = await self.client.post("/", data=data)
+            self.assertEqual(await response.text(), expected)
+            self.assertEqual(response.status, 200)