Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 43dbc2f

Browse files
sgaistCopilotcooperlees
authored
Implement blackd python client (psf#4774)
* feat: implement blackd python client This class allows to easily send code to be formatted by blackd. * chore: fixed CHANGES.md Wrong number * chore: apply suggestions from code review Fixes: - Typos - Wrong option reference Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Cooper Lees <[email protected]>
1 parent 298f8e7 commit 43dbc2f

4 files changed

Lines changed: 229 additions & 2 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646

4747
<!-- Changes to blackd -->
4848

49+
- Implemented BlackDClient. This simple python client allows to easily send formatting
50+
requests to blackd (#4774)
51+
4952
### Integrations
5053

5154
<!-- For example, Docker, GitHub Actions, pre-commit, editors -->

docs/usage_and_configuration/black_as_a_server.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,30 @@ formatting requests.
2727
2828
```
2929

30-
There is no official `blackd` client tool (yet!). You can test that blackd is working
31-
using `curl`:
30+
You can test that blackd is working using `curl`:
3231

3332
```sh
3433
blackd --bind-port 9090 & # or let blackd choose a port
3534
curl -s -XPOST "localhost:9090" -d "print('valid')"
3635
```
3736

37+
Or using the python client:
38+
39+
```python
40+
import asyncio
41+
42+
from blackd.client import BlackDClient
43+
44+
async def main():
45+
client = BlackDClient(url="http://127.0.0.1:9090")
46+
unformatted_code = "def hello(): print('Hello, World!')"
47+
formatted_code = await client.format_code(unformatted_code)
48+
print(formatted_code)
49+
50+
if __name__ == "__main__":
51+
asyncio.run(main())
52+
```
53+
3854
## Protocol
3955

4056
`blackd` only accepts `POST` requests at the `/` path. The body of the request should

src/blackd/client.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from typing import Optional
2+
3+
import aiohttp
4+
from aiohttp.typedefs import StrOrURL
5+
6+
import black
7+
8+
_DEFAULT_HEADERS = {"Content-Type": "text/plain; charset=utf-8"}
9+
10+
11+
class BlackDClient:
12+
def __init__(
13+
self,
14+
url: StrOrURL = "http://localhost:9090",
15+
line_length: Optional[int] = None,
16+
skip_source_first_line: bool = False,
17+
skip_string_normalization: bool = False,
18+
skip_magic_trailing_comma: bool = False,
19+
preview: bool = False,
20+
fast: bool = False,
21+
python_variant: Optional[str] = None,
22+
diff: bool = False,
23+
headers: Optional[dict[str, str]] = None,
24+
):
25+
"""
26+
Initialize a BlackDClient object.
27+
:param url: The URL of the BlackD server.
28+
:param line_length: The maximum line length.
29+
Corresponds to the ``--line-length`` CLI option.
30+
:param skip_source_first_line: True to skip the first line of the source.
31+
Corresponds to the ``--skip-source-first-line`` CLI option.
32+
:param skip_string_normalization: True to skip string normalization.
33+
Corresponds to the ``--skip-string-normalization`` CLI option.
34+
:param skip_magic_trailing_comma: True to skip magic trailing comma.
35+
Corresponds to the ``--skip-magic-trailing-comma`` CLI option.
36+
:param preview: True to enable experimental preview mode.
37+
Corresponds to the ``--preview`` CLI option.
38+
:param fast: True to enable fast mode.
39+
Corresponds to the ``--fast`` CLI option.
40+
:param python_variant: The Python variant to use.
41+
Corresponds to the ``--pyi`` CLI option if this is "pyi".
42+
Otherwise, corresponds to the ``--target-version`` CLI option.
43+
:param diff: True to enable diff mode.
44+
Corresponds to the ``--diff`` CLI option.
45+
:param headers: A dictionary of additional custom headers to send with
46+
the request.
47+
"""
48+
self.url = url
49+
self.headers = _DEFAULT_HEADERS.copy()
50+
51+
if line_length is not None:
52+
self.headers["X-Line-Length"] = str(line_length)
53+
if skip_source_first_line:
54+
self.headers["X-Skip-Source-First-Line"] = "yes"
55+
if skip_string_normalization:
56+
self.headers["X-Skip-String-Normalization"] = "yes"
57+
if skip_magic_trailing_comma:
58+
self.headers["X-Skip-Magic-Trailing-Comma"] = "yes"
59+
if preview:
60+
self.headers["X-Preview"] = "yes"
61+
if fast:
62+
self.headers["X-Fast-Or-Safe"] = "fast"
63+
if python_variant is not None:
64+
self.headers["X-Python-Variant"] = python_variant
65+
if diff:
66+
self.headers["X-Diff"] = "yes"
67+
68+
if headers is not None:
69+
self.headers.update(headers)
70+
71+
async def format_code(self, unformatted_code: str) -> str:
72+
async with aiohttp.ClientSession() as session:
73+
async with session.post(
74+
self.url, headers=self.headers, data=unformatted_code.encode("utf-8")
75+
) as response:
76+
if response.status == 204:
77+
# Input is already well-formatted
78+
return unformatted_code
79+
elif response.status == 200:
80+
# Formatting was needed
81+
return await response.text()
82+
elif response.status == 400:
83+
# Input contains a syntax error
84+
error_message = await response.text()
85+
raise black.InvalidInput(error_message)
86+
elif response.status == 500:
87+
# Other kind of error while formatting
88+
error_message = await response.text()
89+
raise RuntimeError(f"Error while formatting: {error_message}")
90+
else:
91+
# Unexpected response status code
92+
raise RuntimeError(
93+
f"Unexpected response status code: {response.status}"
94+
)

tests/test_blackd.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
from aiohttp.test_utils import AioHTTPTestCase
1313

1414
import blackd
15+
import blackd.client
1516
except ImportError as e:
1617
raise RuntimeError("Please install Black with the 'd' extra") from e
1718

19+
import black
20+
1821

1922
@pytest.mark.blackd
2023
class BlackDTestCase(AioHTTPTestCase):
@@ -218,3 +221,114 @@ async def test_single_character(self) -> None:
218221
response = await self.client.post("/", data="1")
219222
self.assertEqual(await response.text(), "1\n")
220223
self.assertEqual(response.status, 200)
224+
225+
226+
@pytest.mark.blackd
227+
class BlackDClientTestCase(AioHTTPTestCase):
228+
def tearDown(self) -> None:
229+
# Work around https://github.com/python/cpython/issues/124706
230+
gc.collect()
231+
super().tearDown()
232+
233+
async def get_application(self) -> web.Application:
234+
return blackd.make_app()
235+
236+
async def test_unformatted_code(self) -> None:
237+
client = blackd.client.BlackDClient(self.client.make_url("/"))
238+
unformatted_code = "def hello(): print('Hello, World!')"
239+
expected = 'def hello():\n print("Hello, World!")\n'
240+
formatted_code = await client.format_code(unformatted_code)
241+
242+
self.assertEqual(formatted_code, expected)
243+
244+
async def test_formatted_code(self) -> None:
245+
client = blackd.client.BlackDClient(self.client.make_url("/"))
246+
initial_code = 'def hello():\n print("Hello, World!")\n'
247+
expected = 'def hello():\n print("Hello, World!")\n'
248+
formatted_code = await client.format_code(initial_code)
249+
250+
self.assertEqual(formatted_code, expected)
251+
252+
async def test_line_length(self) -> None:
253+
client = blackd.client.BlackDClient(self.client.make_url("/"), line_length=10)
254+
unformatted_code = "def hello(): print('Hello, World!')"
255+
expected = 'def hello():\n print(\n "Hello, World!"\n )\n'
256+
formatted_code = await client.format_code(unformatted_code)
257+
258+
self.assertEqual(formatted_code, expected)
259+
260+
async def test_skip_source_first_line(self) -> None:
261+
client = blackd.client.BlackDClient(
262+
self.client.make_url("/"), skip_source_first_line=True
263+
)
264+
invalid_first_line = "Header will be skipped\r\ni = [1,2,3]\nj = [1,2,3]\n"
265+
expected_result = "Header will be skipped\r\ni = [1, 2, 3]\nj = [1, 2, 3]\n"
266+
formatted_code = await client.format_code(invalid_first_line)
267+
268+
self.assertEqual(formatted_code, expected_result)
269+
270+
async def test_skip_string_normalization(self) -> None:
271+
client = blackd.client.BlackDClient(
272+
self.client.make_url("/"), skip_string_normalization=True
273+
)
274+
unformatted_code = "def hello(): print('Hello, World!')"
275+
expected = "def hello():\n print('Hello, World!')\n"
276+
formatted_code = await client.format_code(unformatted_code)
277+
278+
self.assertEqual(formatted_code, expected)
279+
280+
async def test_skip_magic_trailing_comma(self) -> None:
281+
client = blackd.client.BlackDClient(
282+
self.client.make_url("/"), skip_magic_trailing_comma=True
283+
)
284+
unformatted_code = "def hello(): print('Hello, World!')"
285+
expected = 'def hello():\n print("Hello, World!")\n'
286+
formatted_code = await client.format_code(unformatted_code)
287+
288+
self.assertEqual(formatted_code, expected)
289+
290+
async def test_preview(self) -> None:
291+
client = blackd.client.BlackDClient(self.client.make_url("/"), preview=True)
292+
unformatted_code = "def hello(): print('Hello, World!')"
293+
expected = 'def hello():\n print("Hello, World!")\n'
294+
formatted_code = await client.format_code(unformatted_code)
295+
296+
self.assertEqual(formatted_code, expected)
297+
298+
async def test_fast(self) -> None:
299+
client = blackd.client.BlackDClient(self.client.make_url("/"), fast=True)
300+
unformatted_code = "def hello(): print('Hello, World!')"
301+
expected = 'def hello():\n print("Hello, World!")\n'
302+
formatted_code = await client.format_code(unformatted_code)
303+
304+
self.assertEqual(formatted_code, expected)
305+
306+
async def test_python_variant(self) -> None:
307+
client = blackd.client.BlackDClient(
308+
self.client.make_url("/"), python_variant="3.6"
309+
)
310+
unformatted_code = "def hello(): print('Hello, World!')"
311+
expected = 'def hello():\n print("Hello, World!")\n'
312+
formatted_code = await client.format_code(unformatted_code)
313+
314+
self.assertEqual(formatted_code, expected)
315+
316+
async def test_diff(self) -> None:
317+
diff_header = re.compile(
318+
r"(In|Out)\t\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d\d\d\d\+\d\d:\d\d"
319+
)
320+
321+
client = blackd.client.BlackDClient(self.client.make_url("/"), diff=True)
322+
source, _ = read_data("miscellaneous", "blackd_diff")
323+
expected, _ = read_data("miscellaneous", "blackd_diff.diff")
324+
325+
diff = await client.format_code(source)
326+
diff = diff_header.sub(DETERMINISTIC_HEADER, diff)
327+
328+
self.assertEqual(diff, expected)
329+
330+
async def test_syntax_error(self) -> None:
331+
client = blackd.client.BlackDClient(self.client.make_url("/"))
332+
with_syntax_error = "def hello(): a 'Hello, World!'"
333+
with self.assertRaises(black.InvalidInput):
334+
_ = await client.format_code(with_syntax_error)

0 commit comments

Comments
 (0)