Add diff support to blackd (#969)

This commit is contained in:
Joe Antonakakis 2019-10-28 06:25:26 -07:00 committed by Łukasz Langa
parent b65af236cf
commit df6e1a41f7
5 changed files with 70 additions and 2 deletions

View File

@ -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: 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-Line-Length`: corresponds to the `--line-length` command line flag.
- `X-Skip-String-Normalization`: corresponds to the `--skip-string-normalization` - `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 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, 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 to request code that is compatible with Python 3.5 and 3.6, set the header to
`py3.5,py3.6`. `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 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. 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` 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 ### 19.3b0
- new option `--target-version` to control which Python versions _Black_-formatted code - new option `--target-version` to control which Python versions _Black_-formatted code

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
from concurrent.futures import Executor, ProcessPoolExecutor from concurrent.futures import Executor, ProcessPoolExecutor
from datetime import datetime
from functools import partial from functools import partial
import logging import logging
from multiprocessing import freeze_support from multiprocessing import freeze_support
@ -21,6 +22,7 @@
PYTHON_VARIANT_HEADER = "X-Python-Variant" PYTHON_VARIANT_HEADER = "X-Python-Variant"
SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization" SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization"
FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe" FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe"
DIFF_HEADER = "X-Diff"
BLACK_HEADERS = [ BLACK_HEADERS = [
PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION_HEADER,
@ -28,6 +30,7 @@
PYTHON_VARIANT_HEADER, PYTHON_VARIANT_HEADER,
SKIP_STRING_NORMALIZATION_HEADER, SKIP_STRING_NORMALIZATION_HEADER,
FAST_OR_SAFE_HEADER, FAST_OR_SAFE_HEADER,
DIFF_HEADER,
] ]
# Response headers # Response headers
@ -112,10 +115,25 @@ async def handle(request: web.Request, executor: Executor) -> web.Response:
req_bytes = await request.content.read() req_bytes = await request.content.read()
charset = request.charset if request.charset is not None else "utf8" charset = request.charset if request.charset is not None else "utf8"
req_str = req_bytes.decode(charset) req_str = req_bytes.decode(charset)
then = datetime.utcnow()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
formatted_str = await loop.run_in_executor( formatted_str = await loop.run_in_executor(
executor, partial(black.format_file_contents, req_str, fast=fast, mode=mode) 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( return web.Response(
content_type=request.content_type, content_type=request.content_type,
charset=charset, charset=charset,

View File

@ -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")
+

View File

@ -0,0 +1,6 @@
def abc ():
return ["hello", "world",
"!"]
print( "Incorrect formatting"
)

View File

@ -35,6 +35,7 @@
fs = partial(black.format_str, mode=black.FileMode()) fs = partial(black.format_str, mode=black.FileMode())
THIS_FILE = Path(__file__) THIS_FILE = Path(__file__)
THIS_DIR = THIS_FILE.parent THIS_DIR = THIS_FILE.parent
DETERMINISTIC_HEADER = "[Deterministic header]"
EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)" EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)"
PY36_ARGS = [ PY36_ARGS = [
f"--target-version={version.name.lower()}" for version in black.PY36_VERSIONS f"--target-version={version.name.lower()}" for version in black.PY36_VERSIONS
@ -259,7 +260,7 @@ def test_piping_diff(self) -> None:
black.main, args, input=BytesIO(source.encode("utf8")) black.main, args, input=BytesIO(source.encode("utf8"))
) )
self.assertEqual(result.exit_code, 0) 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 actual = actual.rstrip() + "\n" # the diff output has a trailing space
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
@ -340,7 +341,7 @@ def test_expression_diff(self) -> None:
finally: finally:
os.unlink(tmp_file) os.unlink(tmp_file)
actual = result.output 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 actual = actual.rstrip() + "\n" # the diff output has a trailing space
if expected != actual: if expected != actual:
dump = black.dump_to_file(actual) dump = black.dump_to_file(actual)
@ -1689,6 +1690,27 @@ async def test_blackd_pyi(self) -> None:
self.assertEqual(response.status, 200) self.assertEqual(response.status, 200)
self.assertEqual(await response.text(), expected) 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") @skip_if_exception("ClientOSError")
@unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed") @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
@unittest_run_loop @unittest_run_loop