]> git.madduck.net Git - etc/vim.git/blob - blackd.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Explicit # fmt: on/off indentation level (#554)
[etc/vim.git] / blackd.py
1 import asyncio
2 from concurrent.futures import Executor, ProcessPoolExecutor
3 from functools import partial
4 import logging
5
6 from aiohttp import web
7 import black
8 import click
9
10 # This is used internally by tests to shut down the server prematurely
11 _stop_signal = asyncio.Event()
12
13 VERSION_HEADER = "X-Protocol-Version"
14 LINE_LENGTH_HEADER = "X-Line-Length"
15 PYTHON_VARIANT_HEADER = "X-Python-Variant"
16 SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization"
17 SKIP_NUMERIC_UNDERSCORE_NORMALIZATION_HEADER = "X-Skip-Numeric-Underscore-Normalization"
18 FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe"
19
20
21 @click.command(context_settings={"help_option_names": ["-h", "--help"]})
22 @click.option(
23     "--bind-host", type=str, help="Address to bind the server to.", default="localhost"
24 )
25 @click.option("--bind-port", type=int, help="Port to listen on", default=45484)
26 @click.version_option(version=black.__version__)
27 def main(bind_host: str, bind_port: int) -> None:
28     logging.basicConfig(level=logging.INFO)
29     app = make_app()
30     ver = black.__version__
31     black.out(f"blackd version {ver} listening on {bind_host} port {bind_port}")
32     web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None)
33
34
35 def make_app() -> web.Application:
36     app = web.Application()
37     executor = ProcessPoolExecutor()
38     app.add_routes([web.post("/", partial(handle, executor=executor))])
39     return app
40
41
42 async def handle(request: web.Request, executor: Executor) -> web.Response:
43     try:
44         if request.headers.get(VERSION_HEADER, "1") != "1":
45             return web.Response(
46                 status=501, text="This server only supports protocol version 1"
47             )
48         try:
49             line_length = int(
50                 request.headers.get(LINE_LENGTH_HEADER, black.DEFAULT_LINE_LENGTH)
51             )
52         except ValueError:
53             return web.Response(status=400, text="Invalid line length header value")
54         py36 = False
55         pyi = False
56         if PYTHON_VARIANT_HEADER in request.headers:
57             value = request.headers[PYTHON_VARIANT_HEADER]
58             if value == "pyi":
59                 pyi = True
60             else:
61                 try:
62                     major, *rest = value.split(".")
63                     if int(major) == 3 and len(rest) > 0:
64                         if int(rest[0]) >= 6:
65                             py36 = True
66                 except ValueError:
67                     return web.Response(
68                         status=400, text=f"Invalid value for {PYTHON_VARIANT_HEADER}"
69                     )
70         skip_string_normalization = bool(
71             request.headers.get(SKIP_STRING_NORMALIZATION_HEADER, False)
72         )
73         skip_numeric_underscore_normalization = bool(
74             request.headers.get(SKIP_NUMERIC_UNDERSCORE_NORMALIZATION_HEADER, False)
75         )
76         fast = False
77         if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast":
78             fast = True
79         mode = black.FileMode.from_configuration(
80             py36=py36,
81             pyi=pyi,
82             skip_string_normalization=skip_string_normalization,
83             skip_numeric_underscore_normalization=skip_numeric_underscore_normalization,
84         )
85         req_bytes = await request.content.read()
86         charset = request.charset if request.charset is not None else "utf8"
87         req_str = req_bytes.decode(charset)
88         loop = asyncio.get_event_loop()
89         formatted_str = await loop.run_in_executor(
90             executor,
91             partial(
92                 black.format_file_contents,
93                 req_str,
94                 line_length=line_length,
95                 fast=fast,
96                 mode=mode,
97             ),
98         )
99         return web.Response(
100             content_type=request.content_type, charset=charset, text=formatted_str
101         )
102     except black.NothingChanged:
103         return web.Response(status=204)
104     except black.InvalidInput as e:
105         return web.Response(status=400, text=str(e))
106     except Exception as e:
107         logging.exception("Exception during handling a request")
108         return web.Response(status=500, text=str(e))
109
110
111 if __name__ == "__main__":
112     black.patch_click()
113     main()