From df6e1a41f79f462645f66f61d4d8785a415deb95 Mon Sep 17 00:00:00 2001 From: Joe Antonakakis Date: Mon, 28 Oct 2019 06:25:26 -0700 Subject: [PATCH] Add diff support to blackd (#969) --- README.md | 8 ++++++++ blackd.py | 18 ++++++++++++++++++ tests/data/blackd_diff.diff | 14 ++++++++++++++ tests/data/blackd_diff.py | 6 ++++++ tests/test_black.py | 26 ++++++++++++++++++++++++-- 5 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/data/blackd_diff.diff create mode 100644 tests/data/blackd_diff.py diff --git a/README.md b/README.md index 8dceeac..8ce3103 100644 --- a/README.md +++ b/README.md @@ -838,6 +838,9 @@ which if present, should have the value `1`, otherwise the request is rejected w The headers controlling how code is formatted are: +If any of these headers are set to invalid values, `blackd` returns a `HTTP 400` error +response, mentioning the name of the problematic header in the message body. + - `X-Line-Length`: corresponds to the `--line-length` command line flag. - `X-Skip-String-Normalization`: corresponds to the `--skip-string-normalization` command line flag. If present and its value is not the empty string, no string @@ -849,6 +852,8 @@ The headers controlling how code is formatted are: a set of comma-separated Python versions, optionally prefixed with `py`. For example, to request code that is compatible with Python 3.5 and 3.6, set the header to `py3.5,py3.6`. +- `X-Diff`: corresponds to the `--diff` command line flag. If present, a diff of the + formats will be output. If any of these headers are set to invalid values, `blackd` returns a `HTTP 400` error response, mentioning the name of the problematic header in the message body. @@ -1034,6 +1039,9 @@ More details can be found in [CONTRIBUTING](CONTRIBUTING.md). - `blackd` now returns the version of _Black_ in the response headers (#1013) +- `blackd` can now output the diff of formats on source code when the `X-Diff` header is + provided (#969) + ### 19.3b0 - new option `--target-version` to control which Python versions _Black_-formatted code diff --git a/blackd.py b/blackd.py index a6c6e8d..d79bfe7 100644 --- a/blackd.py +++ b/blackd.py @@ -1,5 +1,6 @@ import asyncio from concurrent.futures import Executor, ProcessPoolExecutor +from datetime import datetime from functools import partial import logging from multiprocessing import freeze_support @@ -21,6 +22,7 @@ LINE_LENGTH_HEADER = "X-Line-Length" PYTHON_VARIANT_HEADER = "X-Python-Variant" SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization" FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe" +DIFF_HEADER = "X-Diff" BLACK_HEADERS = [ PROTOCOL_VERSION_HEADER, @@ -28,6 +30,7 @@ BLACK_HEADERS = [ PYTHON_VARIANT_HEADER, SKIP_STRING_NORMALIZATION_HEADER, FAST_OR_SAFE_HEADER, + DIFF_HEADER, ] # Response headers @@ -112,10 +115,25 @@ async def handle(request: web.Request, executor: Executor) -> web.Response: req_bytes = await request.content.read() charset = request.charset if request.charset is not None else "utf8" req_str = req_bytes.decode(charset) + then = datetime.utcnow() + loop = asyncio.get_event_loop() formatted_str = await loop.run_in_executor( executor, partial(black.format_file_contents, req_str, fast=fast, mode=mode) ) + + # Only output the diff in the HTTP response + only_diff = bool(request.headers.get(DIFF_HEADER, False)) + if only_diff: + now = datetime.utcnow() + src_name = f"In\t{then} +0000" + dst_name = f"Out\t{now} +0000" + loop = asyncio.get_event_loop() + formatted_str = await loop.run_in_executor( + executor, + partial(black.diff, req_str, formatted_str, src_name, dst_name), + ) + return web.Response( content_type=request.content_type, charset=charset, diff --git a/tests/data/blackd_diff.diff b/tests/data/blackd_diff.diff new file mode 100644 index 0000000..c1aa52e --- /dev/null +++ b/tests/data/blackd_diff.diff @@ -0,0 +1,14 @@ +--- [Deterministic header] ++++ [Deterministic header] +@@ -1,7 +1,6 @@ +-def abc (): +- return ["hello", "world", +- "!"] ++def abc(): ++ return ["hello", "world", "!"] + +-print( "Incorrect formatting" +-) + ++print("Incorrect formatting") ++ \ No newline at end of file diff --git a/tests/data/blackd_diff.py b/tests/data/blackd_diff.py new file mode 100644 index 0000000..c527832 --- /dev/null +++ b/tests/data/blackd_diff.py @@ -0,0 +1,6 @@ +def abc (): + return ["hello", "world", + "!"] + +print( "Incorrect formatting" +) diff --git a/tests/test_black.py b/tests/test_black.py index e6a6647..d4c88d5 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -35,6 +35,7 @@ ff = partial(black.format_file_in_place, mode=black.FileMode(), fast=True) fs = partial(black.format_str, mode=black.FileMode()) THIS_FILE = Path(__file__) THIS_DIR = THIS_FILE.parent +DETERMINISTIC_HEADER = "[Deterministic header]" EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)" PY36_ARGS = [ f"--target-version={version.name.lower()}" for version in black.PY36_VERSIONS @@ -259,7 +260,7 @@ class BlackTestCase(unittest.TestCase): black.main, args, input=BytesIO(source.encode("utf8")) ) self.assertEqual(result.exit_code, 0) - actual = diff_header.sub("[Deterministic header]", result.output) + actual = diff_header.sub(DETERMINISTIC_HEADER, result.output) actual = actual.rstrip() + "\n" # the diff output has a trailing space self.assertEqual(expected, actual) @@ -340,7 +341,7 @@ class BlackTestCase(unittest.TestCase): finally: os.unlink(tmp_file) actual = result.output - actual = diff_header.sub("[Deterministic header]", actual) + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) actual = actual.rstrip() + "\n" # the diff output has a trailing space if expected != actual: dump = black.dump_to_file(actual) @@ -1689,6 +1690,27 @@ class BlackDTestCase(AioHTTPTestCase): self.assertEqual(response.status, 200) self.assertEqual(await response.text(), expected) + @skip_if_exception("ClientOSError") + @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") + @unittest_run_loop + async def test_blackd_diff(self) -> None: + diff_header = re.compile( + rf"(In|Out)\t\d\d\d\d-\d\d-\d\d " + rf"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d" + ) + + source, _ = read_data("blackd_diff.py") + expected, _ = read_data("blackd_diff.diff") + + response = await self.client.post( + "/", data=source, headers={blackd.DIFF_HEADER: "true"} + ) + self.assertEqual(response.status, 200) + + actual = await response.text() + actual = diff_header.sub(DETERMINISTIC_HEADER, actual) + self.assertEqual(actual, expected) + @skip_if_exception("ClientOSError") @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest_run_loop -- 2.39.5