]> 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:

Move things around in change log for the latest version to sort in rough notability...
[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 FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe"
18
19
20 @click.command(context_settings={"help_option_names": ["-h", "--help"]})
21 @click.option(
22     "--bind-host", type=str, help="Address to bind the server to.", default="localhost"
23 )
24 @click.option("--bind-port", type=int, help="Port to listen on", default=45484)
25 @click.version_option(version=black.__version__)
26 def main(bind_host: str, bind_port: int) -> None:
27     logging.basicConfig(level=logging.INFO)
28     app = make_app()
29     ver = black.__version__
30     black.out(f"blackd version {ver} listening on {bind_host} port {bind_port}")
31     web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None)
32
33
34 def make_app() -> web.Application:
35     app = web.Application()
36     executor = ProcessPoolExecutor()
37     app.add_routes([web.post("/", partial(handle, executor=executor))])
38     return app
39
40
41 async def handle(request: web.Request, executor: Executor) -> web.Response:
42     try:
43         if request.headers.get(VERSION_HEADER, "1") != "1":
44             return web.Response(
45                 status=501, text="This server only supports protocol version 1"
46             )
47         try:
48             line_length = int(
49                 request.headers.get(LINE_LENGTH_HEADER, black.DEFAULT_LINE_LENGTH)
50             )
51         except ValueError:
52             return web.Response(status=400, text="Invalid line length header value")
53         py36 = False
54         pyi = False
55         if PYTHON_VARIANT_HEADER in request.headers:
56             value = request.headers[PYTHON_VARIANT_HEADER]
57             if value == "pyi":
58                 pyi = True
59             else:
60                 try:
61                     major, *rest = value.split(".")
62                     if int(major) == 3 and len(rest) > 0:
63                         if int(rest[0]) >= 6:
64                             py36 = True
65                 except ValueError:
66                     return web.Response(
67                         status=400, text=f"Invalid value for {PYTHON_VARIANT_HEADER}"
68                     )
69         skip_string_normalization = bool(
70             request.headers.get(SKIP_STRING_NORMALIZATION_HEADER, False)
71         )
72         fast = False
73         if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast":
74             fast = True
75         mode = black.FileMode.from_configuration(
76             py36=py36, pyi=pyi, skip_string_normalization=skip_string_normalization
77         )
78         req_bytes = await request.content.read()
79         charset = request.charset if request.charset is not None else "utf8"
80         req_str = req_bytes.decode(charset)
81         loop = asyncio.get_event_loop()
82         formatted_str = await loop.run_in_executor(
83             executor,
84             partial(
85                 black.format_file_contents,
86                 req_str,
87                 line_length=line_length,
88                 fast=fast,
89                 mode=mode,
90             ),
91         )
92         return web.Response(
93             content_type=request.content_type, charset=charset, text=formatted_str
94         )
95     except black.NothingChanged:
96         return web.Response(status=204)
97     except black.InvalidInput as e:
98         return web.Response(status=400, text=str(e))
99     except Exception as e:
100         logging.exception("Exception during handling a request")
101         return web.Response(status=500, text=str(e))
102
103
104 if __name__ == "__main__":
105     black.patch_click()
106     main()