]> git.madduck.net Git - etc/vim.git/commitdiff

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:

blackd: a HTTP server for blackening (#460)
authorZsolt Dollenstein <zsol.zsol@gmail.com>
Mon, 17 Sep 2018 17:02:25 +0000 (18:02 +0100)
committerŁukasz Langa <lukasz@langa.pl>
Mon, 17 Sep 2018 17:02:25 +0000 (10:02 -0700)
13 files changed:
.appveyor.yml
.travis.yml
Pipfile
Pipfile.lock
README.md
black.py
blackd.py [new file with mode: 0644]
docs/blackd.md [new symlink]
docs/index.rst
mypy.ini
pyproject.toml
setup.py
tests/test_black.py

index 5003ce01151d88e54969febcaa5c3042a69fb80c..0e5582b20c73a63c65b096ebe67cc078db447081 100644 (file)
@@ -1,10 +1,10 @@
 install:
 - C:\Python36\python.exe -m pip install mypy
 install:
 - C:\Python36\python.exe -m pip install mypy
-- C:\Python36\python.exe -m pip install -e .
+- C:\Python36\python.exe -m pip install -e .[d]
 
 # Not a C# project
 build: off
 
 test_script:
 - C:\Python36\python.exe tests/test_black.py
 
 # Not a C# project
 build: off
 
 test_script:
 - C:\Python36\python.exe tests/test_black.py
-- C:\Python36\python.exe -m mypy black.py tests/test_black.py
+- C:\Python36\python.exe -m mypy black.py blackd.py tests/test_black.py
index 09c7251779a439034f862bf30563717726b41b16..04c0c08c95a0a338cde499b369ff0bb2fbc60d33 100644 (file)
@@ -4,12 +4,12 @@ language: python
 cache: pip
 install:
 - pip install coverage coveralls flake8 flake8-bugbear mypy
 cache: pip
 install:
 - pip install coverage coveralls flake8 flake8-bugbear mypy
-- pip install -e .
+- pip install -e '.[d]'
 script:
 - coverage run tests/test_black.py
 script:
 - coverage run tests/test_black.py
-- if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then mypy black.py tests/test_black.py; fi
+- if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then mypy black.py blackd.py tests/test_black.py; fi
 - if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then black --check --verbose .; fi
 - if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then black --check --verbose .; fi
-- if [[ $TRAVIS_PYTHON_VERSION == '3.8-dev' ]]; then flake8 black.py tests/test_black.py; fi
+- if [[ $TRAVIS_PYTHON_VERSION == '3.8-dev' ]]; then flake8 black.py blackd.py tests/test_black.py; fi
 after_success:
 - coveralls
 notifications:
 after_success:
 - coveralls
 notifications:
diff --git a/Pipfile b/Pipfile
index 6ac3f51fa5476dd9a776f7a7b5fb7eacc80e1241..e35462ea50254080d2982c989d7197f7a6c21f21 100644 (file)
--- a/Pipfile
+++ b/Pipfile
@@ -4,10 +4,12 @@ verify_ssl = true
 name = "pypi"
 
 [packages]
 name = "pypi"
 
 [packages]
+aiohttp = ">=3.3.2"
 attrs = ">=17.4.0"
 click = ">=6.5"
 appdirs = "*"
 toml = ">=0.9.4"
 attrs = ">=17.4.0"
 click = ">=6.5"
 appdirs = "*"
 toml = ">=0.9.4"
+black = {editable = true, path = ".", extras = ["d"]}
 
 [dev-packages]
 pre-commit = "*"
 
 [dev-packages]
 pre-commit = "*"
index e976244f072ccfe515c630fd2bcb7adbc9139d70..a47a6a897b38f50c0074fa1d0c2e3c1da1a19753 100644 (file)
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
 {
     "_meta": {
         "hash": {
-            "sha256": "83e7921ad3bcb0c1ee6c654ef6c2b737dd72629c2541f387ac186bedc55d33af"
+            "sha256": "1becc24ea9195b4295de408a96a019ad449aaee7da84555b6857bb2125749892"
         },
         "pipfile-spec": 6,
         "requires": {},
         },
         "pipfile-spec": 6,
         "requires": {},
         ]
     },
     "default": {
         ]
     },
     "default": {
+        "aiohttp": {
+            "hashes": [
+                "sha256:01a2059a0505460828854d218cf090d80db277033b8e6906144ab9bd4677fc82",
+                "sha256:01bcaf83911c5a88f74629f116540a1b80391e6e496e6fb8708bb2987b60da63",
+                "sha256:199ea4a9c424904f04a86563a8e9e2759d49e3a0bf789496714253237f16015f",
+                "sha256:229975cb8ff6056c8ef581383a653e7110480d52c9f46eaf560113f8d5005510",
+                "sha256:2bb4224e3a3d7dd2ee18f6c42c1925c3200cd46fe18ec9f293b9bc88644c4878",
+                "sha256:2ddf47c31048efad5a566d82822194bbb680fc1be852915c2949eb69891b5d5a",
+                "sha256:3bc9c87845962f583d6929f837b02b80d2544920be65daf0d0a1306ad1a2089b",
+                "sha256:3f88a3428f40c788321cf5b8191f9dd9e002145545fa0cefc023b4b11e17aaa7",
+                "sha256:4785935328facee0878c29d46f02b12f1e8e8db1cd3d9ec9af666eb163418a64",
+                "sha256:48e8d1973ba62a952f19a7916e54a7155f4b14505507432fc0559d8b5b0e5cad",
+                "sha256:5cd8662ddd7c95e99010e30cc52e20a092939844e8e8a4f37abf1866231f1880",
+                "sha256:6880406a0c776fbff63c0d9eb8a2d96d8134b17fafeeea01180b58ab8ff0f6f5",
+                "sha256:6a8e447742fc45791ffea0b3ce308f1476a9f4707fb6525a2f23b43d4b26cfb3",
+                "sha256:81456c04c54288928da4e7e1893314c8e74d5e9f33163e39aa47c26c5e5c7911",
+                "sha256:9b15efa7411dcf3b59c1f4766eb16ba1aba4531a33e54d469ee22106eabce460",
+                "sha256:a6132db365def76145084041cede574a0c8ed53aa1a680a3027e41ee8f291bd4",
+                "sha256:ddee38858a9ef52ca33cb5dd1607d07d0fb99e2efe523ecb437b1758c49622a5",
+                "sha256:de703f333381864dce788dbfa1a49ef4551e8f082b607a943b94b239d97965cc",
+                "sha256:e08cacfede41291c05b4668c3178d303d078417c013bc3d5287b2b0d0e6a3aa7",
+                "sha256:e4c37c7ec1e1157ae4af73fd1d7f201accebf6ed2222120bc660fd002c45cbac",
+                "sha256:e4f9fc91d617d2e54bda97bc1db9814918691fe799e037ccf973fda434fd2c18",
+                "sha256:f6f73c812c1830a06de76ccbea10a4ebb1fd46230a80f280362e84578e4932a2"
+            ],
+            "index": "pypi",
+            "version": "==3.4.0"
+        },
         "appdirs": {
             "hashes": [
                 "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
         "appdirs": {
             "hashes": [
                 "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
             "index": "pypi",
             "version": "==1.4.3"
         },
             "index": "pypi",
             "version": "==1.4.3"
         },
+        "async-timeout": {
+            "hashes": [
+                "sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c",
+                "sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287"
+            ],
+            "markers": "python_version >= '3.5.3'",
+            "version": "==3.0.0"
+        },
         "attrs": {
             "hashes": [
                 "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
         "attrs": {
             "hashes": [
                 "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
             "index": "pypi",
             "version": "==18.1.0"
         },
             "index": "pypi",
             "version": "==18.1.0"
         },
+        "black": {
+            "editable": true,
+            "extras": [
+                "d"
+            ],
+            "path": "."
+        },
+        "chardet": {
+            "hashes": [
+                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
+                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+            ],
+            "version": "==3.0.4"
+        },
         "click": {
             "hashes": [
                 "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
         "click": {
             "hashes": [
                 "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
             "index": "pypi",
             "version": "==6.7"
         },
             "index": "pypi",
             "version": "==6.7"
         },
+        "idna": {
+            "hashes": [
+                "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
+                "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
+            ],
+            "version": "==2.7"
+        },
+        "idna-ssl": {
+            "hashes": [
+                "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"
+            ],
+            "markers": "python_version < '3.7'",
+            "version": "==1.1.0"
+        },
+        "multidict": {
+            "hashes": [
+                "sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
+                "sha256:1d6e191965505652f194bc4c40270a842922685918a4f45e6936a6b15cc5816d",
+                "sha256:295961a6a88f1199e19968e15d9b42f3a191c89ec13034dbc212bf9c394c3c82",
+                "sha256:2be5af084de6c3b8e20d6421cb0346378a9c867dcf7c86030d6b0b550f9888e4",
+                "sha256:2eb99617c7a0e9f2b90b64bc1fb742611718618572747d6f3d6532b7b78755ab",
+                "sha256:4ba654c6b5ad1ae4a4d792abeb695b29ce981bb0f157a41d0fd227b385f2bef0",
+                "sha256:5ba766433c30d703f6b2c17eb0b6826c6f898e5f58d89373e235f07764952314",
+                "sha256:a59d58ee85b11f337b54933e8d758b2356fcdcc493248e004c9c5e5d11eedbe4",
+                "sha256:a6e35d28900cf87bcc11e6ca9e474db0099b78f0be0a41d95bef02d49101b5b2",
+                "sha256:b4df7ca9c01018a51e43937eaa41f2f5dce17a6382fda0086403bcb1f5c2cf8e",
+                "sha256:bbd5a6bffd3ba8bfe75b16b5e28af15265538e8be011b0b9fddc7d86a453fd4a",
+                "sha256:d870f399fcd58a1889e93008762a3b9a27cf7ea512818fc6e689f59495648355",
+                "sha256:e9404e2e19e901121c3c5c6cffd5a8ae0d1d67919c970e3b3262231175713068"
+            ],
+            "markers": "python_version >= '3.4.1'",
+            "version": "==4.3.1"
+        },
         "toml": {
             "hashes": [
                 "sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
             ],
             "index": "pypi",
             "version": "==0.9.4"
         "toml": {
             "hashes": [
                 "sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d"
             ],
             "index": "pypi",
             "version": "==0.9.4"
+        },
+        "yarl": {
+            "hashes": [
+                "sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9",
+                "sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee",
+                "sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308",
+                "sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357",
+                "sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78",
+                "sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8",
+                "sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1",
+                "sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4",
+                "sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7"
+            ],
+            "markers": "python_version >= '3.4.1'",
+            "version": "==1.2.6"
         }
     },
     "develop": {
         }
     },
     "develop": {
         },
         "bleach": {
             "hashes": [
         },
         "bleach": {
             "hashes": [
-                "sha256:b8fa79e91f96c2c2cd9fd1f9eda906efb1b88b483048978ba62fef680e962b34",
-                "sha256:eb7386f632349d10d9ce9d4a838b134d4731571851149f9cc2c05a9a837a9a44"
+                "sha256:0ee95f6167129859c5dce9b1ca291ebdb5d8cd7e382ca0e237dfd0dad63f63d8",
+                "sha256:24754b9a7d530bf30ce7cbc805bc6cce785660b4a10ff3a43633728438c105ab"
             ],
             ],
-            "version": "==2.1.3"
+            "version": "==2.1.4"
         },
         "cached-property": {
             "hashes": [
         },
         "cached-property": {
             "hashes": [
         },
         "certifi": {
             "hashes": [
         },
         "certifi": {
             "hashes": [
-                "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
-                "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
+                "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
+                "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
             ],
             ],
-            "version": "==2018.4.16"
+            "version": "==2018.8.24"
         },
         "cffi": {
             "hashes": [
         },
         "cffi": {
             "hashes": [
             "hashes": [
                 "sha256:0186dccca79483e3405217993b83b914ba4559fe9a8396efc4eea56561b74061",
                 "sha256:1a625afc6f62da428df96ec325dc30866cc5781520cbd904ff4ec44cf018171c",
             "hashes": [
                 "sha256:0186dccca79483e3405217993b83b914ba4559fe9a8396efc4eea56561b74061",
                 "sha256:1a625afc6f62da428df96ec325dc30866cc5781520cbd904ff4ec44cf018171c",
+                "sha256:207b7673ff4e177374c572feeae0e4ef33be620ec9171c08fd22e2b796e03e3d",
                 "sha256:275905bb371a99285c74931700db3f0c078e7603bed383e8cf1a09f3ee05a3de",
                 "sha256:50098f1c4950722521f0671e54139e0edc1837d63c990cf0f3d2c49607bb51a2",
                 "sha256:50ed116d0b60a07df0dc7b180c28569064b9d37d1578d4c9021cff04d725cb63",
                 "sha256:275905bb371a99285c74931700db3f0c078e7603bed383e8cf1a09f3ee05a3de",
                 "sha256:50098f1c4950722521f0671e54139e0edc1837d63c990cf0f3d2c49607bb51a2",
                 "sha256:50ed116d0b60a07df0dc7b180c28569064b9d37d1578d4c9021cff04d725cb63",
                 "sha256:811527e9b7280b136734ed6cb6845e5fbccaeaa132ddf45f0246cbe544016957",
                 "sha256:987b0e157f70c72a84f3c2f9ef2d7ab0f26c08f2bf326c12c087ff9eebcb3ff5",
                 "sha256:9fc6a2183d0a9b0974ec7cdcdad42bd78a3be674cc3e65f87dd694419b3b0ab7",
                 "sha256:811527e9b7280b136734ed6cb6845e5fbccaeaa132ddf45f0246cbe544016957",
                 "sha256:987b0e157f70c72a84f3c2f9ef2d7ab0f26c08f2bf326c12c087ff9eebcb3ff5",
                 "sha256:9fc6a2183d0a9b0974ec7cdcdad42bd78a3be674cc3e65f87dd694419b3b0ab7",
+                "sha256:a3d17ee4ae739fe16f7501a52255c2e287ac817cfd88565b9859f70520afffea",
+                "sha256:ba5b5488719c0f2ced0aa1986376f7baff1a1653a8eb5fdfcf3f84c7ce46ef8d",
                 "sha256:c573ea89dd95d41b6d8cf36799c34b6d5b1eac4aed0212dee0f0a11fb7b01e8f",
                 "sha256:c5f1b9e8592d2c448c44e6bc0d91224b16ea5f8293908b1561de1f6d2d0658b1",
                 "sha256:cbe581456357d8f0674d6a590b1aaf46c11d01dd0a23af147a51a798c3818034",
                 "sha256:cf219bec69e601fe27e3974b7307d2f06082ab385d42752738ad2eb630a47d65",
                 "sha256:c573ea89dd95d41b6d8cf36799c34b6d5b1eac4aed0212dee0f0a11fb7b01e8f",
                 "sha256:c5f1b9e8592d2c448c44e6bc0d91224b16ea5f8293908b1561de1f6d2d0658b1",
                 "sha256:cbe581456357d8f0674d6a590b1aaf46c11d01dd0a23af147a51a798c3818034",
                 "sha256:cf219bec69e601fe27e3974b7307d2f06082ab385d42752738ad2eb630a47d65",
+                "sha256:cf5014eb214d814a83a7a47407272d5db10b719dbeaf4d3cfe5969309d0fcf4b",
                 "sha256:d08bad67fa18f7e8ff738c090628ee0cbf0505d74a991c848d6d04abfe67b697",
                 "sha256:d6f716d7b1182bf35862b5065112f933f43dd1aa4f8097c9bcfb246f71528a34",
                 "sha256:e08e479102627641c7cb4ece421c6ed4124820b1758765db32201136762282d9",
                 "sha256:e20ac21418af0298437d29599f7851915497ce9f2866bc8e86b084d8911ee061",
                 "sha256:e25f53c37e319241b9a412382140dffac98ca756ba8f360ac7ab5e30cad9670a",
                 "sha256:d08bad67fa18f7e8ff738c090628ee0cbf0505d74a991c848d6d04abfe67b697",
                 "sha256:d6f716d7b1182bf35862b5065112f933f43dd1aa4f8097c9bcfb246f71528a34",
                 "sha256:e08e479102627641c7cb4ece421c6ed4124820b1758765db32201136762282d9",
                 "sha256:e20ac21418af0298437d29599f7851915497ce9f2866bc8e86b084d8911ee061",
                 "sha256:e25f53c37e319241b9a412382140dffac98ca756ba8f360ac7ab5e30cad9670a",
+                "sha256:e8932bddf159064f04e946fbb64693753488de21586f20e840b3be51745c8c09",
                 "sha256:f20900f16377f2109783ae9348d34bc80530808439591c3d3df73d5c7ef1a00c"
             ],
             "version": "==0.4.2"
                 "sha256:f20900f16377f2109783ae9348d34bc80530808439591c3d3df73d5c7ef1a00c"
             ],
             "version": "==0.4.2"
             "hashes": [
                 "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
                 "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
             "hashes": [
                 "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
                 "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
+                "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
                 "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
                 "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
+                "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
                 "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
                 "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
+                "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
                 "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
                 "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
                 "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
                 "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
                 "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
                 "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
         },
         "flake8-bugbear": {
             "hashes": [
         },
         "flake8-bugbear": {
             "hashes": [
-                "sha256:541746f0f3b2f1a8d7278e1d2d218df298996b60b02677708560db7c7e620e3b",
-                "sha256:5f14a99d458e29cb92be9079c970030e0dd398b2decb179d76d39a5266ea1578"
+                "sha256:07b6e769d7f4e168d590f7088eae40f6ddd9fa4952bed31602def65842682c83",
+                "sha256:0ccf56975f4db1d69dc1cf3598c99d768ebf95d0cad27d76087954aa399b515a"
             ],
             "index": "pypi",
             ],
             "index": "pypi",
-            "version": "==18.2.0"
+            "version": "==18.8.0"
         },
         "flake8-mypy": {
             "hashes": [
         },
         "flake8-mypy": {
             "hashes": [
         },
         "identify": {
             "hashes": [
         },
         "identify": {
             "hashes": [
-                "sha256:0bab212939e3e83caf60788fac9bda5ea4a496f9147ed4746bb04f10ec6c0cbf",
-                "sha256:264f34cbd4002d5b2f4b323ae9e5776a16189363d82627f46570b9c703fea448"
+                "sha256:49845e70fc6b1ec3694ab930a2c558912d7de24548eebcd448f65567dc757c43",
+                "sha256:68daab16a3db364fa204591f97dc40bfffd1a7739f27788a4895b4d8fd3516e5"
             ],
             ],
-            "version": "==1.1.3"
+            "version": "==1.1.4"
         },
         "idna": {
             "hashes": [
         },
         "idna": {
             "hashes": [
         },
         "nodeenv": {
             "hashes": [
         },
         "nodeenv": {
             "hashes": [
-                "sha256:0611c726af1b252908646787f4d49811aa69cd92ec19644ded06ad9d3162f88e"
+                "sha256:aa040ab5189bae17d272175609010be6c5b589ec4b8dbd832cc50c9e9cb7496f"
             ],
             ],
-            "version": "==1.3.1"
+            "version": "==1.3.2"
         },
         "packaging": {
             "hashes": [
         },
         "packaging": {
             "hashes": [
         },
         "pre-commit": {
             "hashes": [
         },
         "pre-commit": {
             "hashes": [
-                "sha256:2d57dd6b0c117ef8363233f256de8a3f26ee1c6e05ed96f9e2d9135ca5467d90",
-                "sha256:9807f29320547a8a13163c1977f6765e488a9349a01431ff4fbd196ff287b51c"
+                "sha256:c1472b0d73e27a5697a477f77a4973c708da4fc433cc89648e8612c8cd623b87",
+                "sha256:ec206de6fbcbd9381ff3169f9975571fb2efa99794c328518a5f0c06ae0b49c5"
             ],
             "index": "pypi",
             ],
             "index": "pypi",
-            "version": "==1.10.3"
+            "version": "==1.10.5"
         },
         "pycodestyle": {
             "hashes": [
         },
         "pycodestyle": {
             "hashes": [
                 "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
                 "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
             ],
                 "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
                 "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
             ],
-            "markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version < '4' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'",
             "version": "==2.19.1"
         },
         "requests-toolbelt": {
             "version": "==2.19.1"
         },
         "requests-toolbelt": {
                 "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
                 "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
             ],
                 "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
                 "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
             ],
-            "markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version < '4' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'",
             "version": "==0.8.0"
         },
         "six": {
             "version": "==0.8.0"
         },
         "six": {
         },
         "sphinx": {
             "hashes": [
         },
         "sphinx": {
             "hashes": [
-                "sha256:217ad9ece2156ed9f8af12b5d2c82a499ddf2c70a33c5f81864a08d8c67b9efc",
-                "sha256:a765c6db1e5b62aae857697cd4402a5c1a315a7b0854bbcd0fc8cdc524da5896"
+                "sha256:71531900af3f68625a29c4e00381bee8f85255219a3d500a3e255076a45b735e",
+                "sha256:a3defde5e17b5bc2aa21820674409287acc4d56bf8d009213d275e4b9d0d490d"
             ],
             "index": "pypi",
             ],
             "index": "pypi",
-            "version": "==1.7.6"
+            "version": "==1.7.7"
         },
         "sphinxcontrib-websupport": {
             "hashes": [
                 "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
                 "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
             ],
         },
         "sphinxcontrib-websupport": {
             "hashes": [
                 "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
                 "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
             ],
-            "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'",
+            "markers": "python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*'",
             "version": "==1.1.0"
         },
         "toml": {
             "version": "==1.1.0"
         },
         "toml": {
         },
         "tqdm": {
             "hashes": [
         },
         "tqdm": {
             "hashes": [
-                "sha256:224291ee0d8c52d91b037fd90806f48c79bcd9994d3b0abc9e44b946a908fccd",
-                "sha256:77b8424d41b31e68f437c6dd9cd567aebc9a860507cb42fbd880a5f822d966fe"
+                "sha256:5ef526702c0d265d5a960a3b27f3971fac13c26cf0fb819294bfa71fc6026c88",
+                "sha256:a3364bd83ce4777320b862e3c8a93d7da91e20a95f06ef79bed7dd71c654cafa"
             ],
             ],
-            "markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'",
-            "version": "==4.23.4"
+            "markers": "python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.6'",
+            "version": "==4.25.0"
         },
         "twine": {
             "hashes": [
         },
         "twine": {
             "hashes": [
                 "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
                 "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
             ],
                 "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
                 "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
             ],
-            "markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version < '4' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'",
+            "markers": "python_version != '3.1.*' and python_version >= '2.6' and python_version < '4' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*'",
             "version": "==1.23"
         },
         "virtualenv": {
             "version": "==1.23"
         },
         "virtualenv": {
                 "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
                 "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
             ],
                 "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
                 "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
             ],
-            "markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7'",
+            "markers": "python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.2.*'",
             "version": "==16.0.0"
         },
         "webencodings": {
             "version": "==16.0.0"
         },
         "webencodings": {
index 6d350d04f8a94c92d9deadab2c7d799cd4ee9b6f..c26ef16dc694ed65755862dfc9fe1f291b852327 100644 (file)
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ Try it out now using the [Black Playground](https://black.now.sh).
 **[Code style](#the-black-code-style)** |
 **[pyproject.toml](#pyprojecttoml)** |
 **[Editor integration](#editor-integration)** |
 **[Code style](#the-black-code-style)** |
 **[pyproject.toml](#pyprojecttoml)** |
 **[Editor integration](#editor-integration)** |
+**[blackd](#blackd)** |
 **[Version control integration](#version-control-integration)** |
 **[Ignoring unmodified files](#ignoring-unmodified-files)** |
 **[Testimonials](#testimonials)** |
 **[Version control integration](#version-control-integration)** |
 **[Ignoring unmodified files](#ignoring-unmodified-files)** |
 **[Testimonials](#testimonials)** |
@@ -745,6 +746,76 @@ affect your use case.
 
 This can be used for example with PyCharm's [File Watchers](https://www.jetbrains.com/help/pycharm/file-watchers.html).
 
 
 This can be used for example with PyCharm's [File Watchers](https://www.jetbrains.com/help/pycharm/file-watchers.html).
 
+## blackd
+
+`blackd` is a small HTTP server that exposes *Black*'s functionality over
+a simple protocol. The main benefit of using it is to avoid paying the
+cost of starting up a new *Black* process every time you want to blacken
+a file.
+
+### Usage
+
+`blackd` is not packaged alongside *Black* by default because it has additional
+dependencies. You will need to do `pip install black[d]` to install it.
+
+You can start the server on the default port, binding only to the local interface
+by running `blackd`. You will see a single line mentioning the server's version,
+and the host and port it's listening on. `blackd` will then print an access log
+similar to most web servers on standard output, merged with any exception traces
+caused by invalid formatting requests.
+
+`blackd` provides even less options than *Black*. You can see them by running
+`blackd --help`:
+
+```text
+Usage: blackd [OPTIONS]
+
+Options:
+  --bind-host TEXT                Address to bind the server to.
+  --bind-port INTEGER             Port to listen on
+  --version                       Show the version and exit.
+  -h, --help                      Show this message and exit.
+```
+
+### Protocol
+
+`blackd` only accepts `POST` requests at the `/` path. The body of the request
+should contain the python source code to be formatted, encoded 
+according to the `charset` field in the `Content-Type` request header. If no
+`charset` is specified, `blackd` assumes `UTF-8`.
+
+There are a few HTTP headers that control how the source is formatted. These
+correspond to command line flags for *Black*. There is one exception to this:
+`X-Protocol-Version` which if present, should have the value `1`, otherwise the
+request is rejected with `HTTP 501` (Not Implemented). 
+
+The headers controlling how code is formatted are:
+
+ - `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
+    normalization will be performed.
+ - `X-Fast-Or-Safe`: if set to `fast`, `blackd` will act as *Black* does when
+    passed the `--fast` command line flag.
+ - `X-Python-Variant`: if set to `pyi`, `blackd` will act as *Black* does when
+    passed the `--pyi` command line flag. Otherwise, its value must correspond to
+    a Python version. If this value represents at least Python 3.6, `blackd` will
+    act as *Black* does when passed the `--py36` command line flag.
+
+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.
+
+Apart from the above, `blackd` can produce the following response codes:
+
+ - `HTTP 204`: If the input is already well-formatted. The response body is
+       empty.
+ - `HTTP 200`: If formatting was needed on the input. The response body
+       contains the blackened Python code, and the `Content-Type` header is set
+       accordingly.
+ - `HTTP 400`: If the input contains a syntax error. Details of the error are
+       returned in the response body.
+ - `HTTP 500`: If there was any kind of error while trying to format the input.
+       The response body contains a textual representation of the error.
 
 ## Version control integration
 
 
 ## Version control integration
 
@@ -850,8 +921,14 @@ More details can be found in [CONTRIBUTING](CONTRIBUTING.md).
 
 ### 18.8b0
 
 
 ### 18.8b0
 
+* added `blackd`, see [its documentation](#blackd) for more info (#349)
+
 * adjacent string literals are now correctly split into multiple lines (#463)
 
 * adjacent string literals are now correctly split into multiple lines (#463)
 
+* added `blackd`, see [its documentation](#blackd) for more info (#349)
+
+* code with `_` in numeric literals is recognized as Python 3.6+ (#461)
+
 * numeric literals are now formatted by *Black* (#452, #461, #464, #469):
 
   * numeric literals are normalized to include `_` separators on Python 3.6+ code
 * numeric literals are now formatted by *Black* (#452, #461, #464, #469):
 
   * numeric literals are normalized to include `_` separators on Python 3.6+ code
index 8d53194680309ce664af0e0a77105c31c2343824..d25756365d574c28f06f5034fb99858bd086f39f 100644 (file)
--- a/black.py
+++ b/black.py
@@ -79,15 +79,15 @@ syms = pygram.python_symbols
 
 
 class NothingChanged(UserWarning):
 
 
 class NothingChanged(UserWarning):
-    """Raised by :func:`format_file` when reformatted code is the same as source."""
+    """Raised when reformatted code is the same as source."""
 
 
 class CannotSplit(Exception):
 
 
 class CannotSplit(Exception):
-    """A readable split that fits the allotted line length is impossible.
+    """A readable split that fits the allotted line length is impossible."""
 
 
-    Raised by :func:`left_hand_split`, :func:`right_hand_split`, and
-    :func:`delimiter_split`.
-    """
+
+class InvalidInput(ValueError):
+    """Raised when input source code fails all parse attempts."""
 
 
 class WriteBack(Enum):
 
 
 class WriteBack(Enum):
@@ -676,7 +676,7 @@ def lib2to3_parse(src_txt: str) -> Node:
                 faulty_line = lines[lineno - 1]
             except IndexError:
                 faulty_line = "<line number missing in source>"
                 faulty_line = lines[lineno - 1]
             except IndexError:
                 faulty_line = "<line number missing in source>"
-            exc = ValueError(f"Cannot parse: {lineno}:{column}: {faulty_line}")
+            exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {faulty_line}")
     else:
         raise exc from None
 
     else:
         raise exc from None
 
diff --git a/blackd.py b/blackd.py
new file mode 100644 (file)
index 0000000..f2bbc8a
--- /dev/null
+++ b/blackd.py
@@ -0,0 +1,106 @@
+import asyncio
+from concurrent.futures import Executor, ProcessPoolExecutor
+from functools import partial
+import logging
+
+from aiohttp import web
+import black
+import click
+
+# This is used internally by tests to shut down the server prematurely
+_stop_signal = asyncio.Event()
+
+VERSION_HEADER = "X-Protocol-Version"
+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"
+
+
+@click.command(context_settings={"help_option_names": ["-h", "--help"]})
+@click.option(
+    "--bind-host", type=str, help="Address to bind the server to.", default="localhost"
+)
+@click.option("--bind-port", type=int, help="Port to listen on", default=45484)
+@click.version_option(version=black.__version__)
+def main(bind_host: str, bind_port: int) -> None:
+    logging.basicConfig(level=logging.INFO)
+    app = make_app()
+    ver = black.__version__
+    black.out(f"blackd version {ver} listening on {bind_host} port {bind_port}")
+    web.run_app(app, host=bind_host, port=bind_port, handle_signals=True, print=None)
+
+
+def make_app() -> web.Application:
+    app = web.Application()
+    executor = ProcessPoolExecutor()
+    app.add_routes([web.post("/", partial(handle, executor=executor))])
+    return app
+
+
+async def handle(request: web.Request, executor: Executor) -> web.Response:
+    try:
+        if request.headers.get(VERSION_HEADER, "1") != "1":
+            return web.Response(
+                status=501, text="This server only supports protocol version 1"
+            )
+        try:
+            line_length = int(
+                request.headers.get(LINE_LENGTH_HEADER, black.DEFAULT_LINE_LENGTH)
+            )
+        except ValueError:
+            return web.Response(status=400, text="Invalid line length header value")
+        py36 = False
+        pyi = False
+        if PYTHON_VARIANT_HEADER in request.headers:
+            value = request.headers[PYTHON_VARIANT_HEADER]
+            if value == "pyi":
+                pyi = True
+            else:
+                try:
+                    major, *rest = value.split(".")
+                    if int(major) == 3 and len(rest) > 0:
+                        if int(rest[0]) >= 6:
+                            py36 = True
+                except ValueError:
+                    return web.Response(
+                        status=400, text=f"Invalid value for {PYTHON_VARIANT_HEADER}"
+                    )
+        skip_string_normalization = bool(
+            request.headers.get(SKIP_STRING_NORMALIZATION_HEADER, False)
+        )
+        fast = False
+        if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast":
+            fast = True
+        mode = black.FileMode.from_configuration(
+            py36=py36, pyi=pyi, skip_string_normalization=skip_string_normalization
+        )
+        req_bytes = await request.content.read()
+        charset = request.charset if request.charset is not None else "utf8"
+        req_str = req_bytes.decode(charset)
+        loop = asyncio.get_event_loop()
+        formatted_str = await loop.run_in_executor(
+            executor,
+            partial(
+                black.format_file_contents,
+                req_str,
+                line_length=line_length,
+                fast=fast,
+                mode=mode,
+            ),
+        )
+        return web.Response(
+            content_type=request.content_type, charset=charset, text=formatted_str
+        )
+    except black.NothingChanged:
+        return web.Response(status=204)
+    except black.InvalidInput as e:
+        return web.Response(status=400, text=str(e))
+    except Exception as e:
+        logging.exception("Exception during handling a request")
+        return web.Response(status=500, text=str(e))
+
+
+if __name__ == "__main__":
+    black.patch_click()
+    main()
diff --git a/docs/blackd.md b/docs/blackd.md
new file mode 120000 (symlink)
index 0000000..015a8e8
--- /dev/null
@@ -0,0 +1 @@
+_build/generated/blackd.md
\ No newline at end of file
index fab9851d9f2c1ce4d5e04cee25a7a8464a623fb1..4291de7e13aa79e15c3a5e2f5721fe20d4506c52 100644 (file)
@@ -52,6 +52,7 @@ Contents
    the_black_code_style
    pyproject_toml
    editor_integration
    the_black_code_style
    pyproject_toml
    editor_integration
+   blackd
    version_control_integration
    ignoring_unmodified_files
    contributing
    version_control_integration
    ignoring_unmodified_files
    contributing
index 8fa723634d5597d775436bbfc42892a0e2cfbb61..841b60027da61eb20d26ba5e4305bf622337119f 100644 (file)
--- a/mypy.ini
+++ b/mypy.ini
@@ -29,3 +29,6 @@ check_untyped_defs=True
 
 # No incremental mode
 cache_dir=/dev/null
 
 # No incremental mode
 cache_dir=/dev/null
+
+[mypy-aiohttp.*]
+follow_imports=skip
index aa44ec39dd24b4910cb9def99485c0ec115b45b5..915f6d1b1a3d39cb263fc1789eb4641f5d7cd9ea 100644 (file)
@@ -62,6 +62,7 @@ attrs = "^17.4"
 click = "^6.5"
 toml = "^0.9.4"
 appdirs = "^1.4"
 click = "^6.5"
 toml = "^0.9.4"
 appdirs = "^1.4"
+aiohttp = "^3.4"
 
 [tool.poetry.dev-dependencies]
 Sphinx = "^1.7"
 
 [tool.poetry.dev-dependencies]
 Sphinx = "^1.7"
index ac0f874abe6ad64cb9aeb7caef80c473f8072357..2b255f8276c39720d01fe38240c0210f5e54e066 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -36,12 +36,13 @@ setup(
     author_email="lukasz@langa.pl",
     url="https://github.com/ambv/black",
     license="MIT",
     author_email="lukasz@langa.pl",
     url="https://github.com/ambv/black",
     license="MIT",
-    py_modules=["black"],
+    py_modules=["black", "blackd"],
     packages=["blib2to3", "blib2to3.pgen2"],
     package_data={"blib2to3": ["*.txt"]},
     python_requires=">=3.6",
     zip_safe=False,
     install_requires=["click>=6.5", "attrs>=17.4.0", "appdirs", "toml>=0.9.4"],
     packages=["blib2to3", "blib2to3.pgen2"],
     package_data={"blib2to3": ["*.txt"]},
     python_requires=">=3.6",
     zip_safe=False,
     install_requires=["click>=6.5", "attrs>=17.4.0", "appdirs", "toml>=0.9.4"],
+    extras_require={"d": ["aiohttp>=3.3.2"]},
     test_suite="tests.test_black",
     classifiers=[
         "Development Status :: 4 - Beta",
     test_suite="tests.test_black",
     classifiers=[
         "Development Status :: 4 - Beta",
@@ -56,5 +57,5 @@ setup(
         "Topic :: Software Development :: Libraries :: Python Modules",
         "Topic :: Software Development :: Quality Assurance",
     ],
         "Topic :: Software Development :: Libraries :: Python Modules",
         "Topic :: Software Development :: Quality Assurance",
     ],
-    entry_points={"console_scripts": ["black=black:main"]},
+    entry_points={"console_scripts": ["black=black:main", "blackd=blackd:main [d]"]},
 )
 )
index 85ab2b6ba3e2375c09295eb3d65d9a0fe29fffac..6eaca98d8d05e19374d07fd53ad168f82629e05c 100644 (file)
@@ -2,14 +2,24 @@
 import asyncio
 from concurrent.futures import ThreadPoolExecutor
 from contextlib import contextmanager
 import asyncio
 from concurrent.futures import ThreadPoolExecutor
 from contextlib import contextmanager
-from functools import partial
+from functools import partial, wraps
 from io import BytesIO, TextIOWrapper
 import os
 from pathlib import Path
 import re
 import sys
 from tempfile import TemporaryDirectory
 from io import BytesIO, TextIOWrapper
 import os
 from pathlib import Path
 import re
 import sys
 from tempfile import TemporaryDirectory
-from typing import Any, BinaryIO, Generator, List, Tuple, Iterator
+from typing import (
+    Any,
+    BinaryIO,
+    Callable,
+    Coroutine,
+    Generator,
+    List,
+    Tuple,
+    Iterator,
+    TypeVar,
+)
 import unittest
 from unittest.mock import patch, MagicMock
 
 import unittest
 from unittest.mock import patch, MagicMock
 
@@ -18,6 +28,14 @@ from click.testing import CliRunner
 
 import black
 
 
 import black
 
+try:
+    import blackd
+    from aiohttp.test_utils import TestClient, TestServer
+except ImportError:
+    has_blackd_deps = False
+else:
+    has_blackd_deps = True
+
 
 ll = 88
 ff = partial(black.format_file_in_place, line_length=ll, fast=True)
 
 ll = 88
 ff = partial(black.format_file_in_place, line_length=ll, fast=True)
@@ -25,6 +43,8 @@ fs = partial(black.format_str, line_length=ll)
 THIS_FILE = Path(__file__)
 THIS_DIR = THIS_FILE.parent
 EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)"
 THIS_FILE = Path(__file__)
 THIS_DIR = THIS_FILE.parent
 EMPTY_LINE = "# EMPTY LINE WITH WHITESPACE" + " (this comment will be removed)"
+T = TypeVar("T")
+R = TypeVar("R")
 
 
 def dump_to_stderr(*output: str) -> str:
 
 
 def dump_to_stderr(*output: str) -> str:
@@ -79,6 +99,15 @@ def event_loop(close: bool) -> Iterator[None]:
             loop.close()
 
 
             loop.close()
 
 
+def async_test(f: Callable[..., Coroutine[Any, None, R]]) -> Callable[..., None]:
+    @event_loop(close=True)
+    @wraps(f)
+    def wrapper(*args: Any, **kwargs: Any) -> None:
+        asyncio.get_event_loop().run_until_complete(f(*args, **kwargs))
+
+    return wrapper
+
+
 class BlackRunner(CliRunner):
     """Modify CliRunner so that stderr is not merged with stdout.
 
 class BlackRunner(CliRunner):
     """Modify CliRunner so that stderr is not merged with stdout.
 
@@ -824,7 +853,7 @@ class BlackTestCase(unittest.TestCase):
         actual = black.format_file_contents(different, line_length=ll, fast=False)
         self.assertEqual(expected, actual)
         invalid = "return if you can"
         actual = black.format_file_contents(different, line_length=ll, fast=False)
         self.assertEqual(expected, actual)
         invalid = "return if you can"
-        with self.assertRaises(ValueError) as e:
+        with self.assertRaises(black.InvalidInput) as e:
             black.format_file_contents(invalid, line_length=ll, fast=False)
         self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
 
             black.format_file_contents(invalid, line_length=ll, fast=False)
         self.assertEqual(str(e.exception), "Cannot parse: 1:7: return if you can")
 
@@ -1289,6 +1318,171 @@ class BlackTestCase(unittest.TestCase):
             except RuntimeError as re:
                 self.fail(f"`patch_click()` failed, exception still raised: {re}")
 
             except RuntimeError as re:
                 self.fail(f"`patch_click()` failed, exception still raised: {re}")
 
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_request_needs_formatting(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post("/", data=b"print('hello world')")
+            self.assertEqual(response.status, 200)
+            self.assertEqual(response.charset, "utf8")
+            self.assertEqual(await response.read(), b'print("hello world")\n')
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_request_no_change(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post("/", data=b'print("hello world")\n')
+            self.assertEqual(response.status, 204)
+            self.assertEqual(await response.read(), b"")
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_request_syntax_error(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post("/", data=b"what even ( is")
+            self.assertEqual(response.status, 400)
+            content = await response.text()
+            self.assertTrue(
+                content.startswith("Cannot parse"),
+                msg=f"Expected error to start with 'Cannot parse', got {repr(content)}",
+            )
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_unsupported_version(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post(
+                "/", data=b"what", headers={blackd.VERSION_HEADER: "2"}
+            )
+            self.assertEqual(response.status, 501)
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_supported_version(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post(
+                "/", data=b"what", headers={blackd.VERSION_HEADER: "1"}
+            )
+            self.assertEqual(response.status, 200)
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_invalid_python_variant(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post(
+                "/", data=b"what", headers={blackd.PYTHON_VARIANT_HEADER: "lol"}
+            )
+            self.assertEqual(response.status, 400)
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_pyi(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            source, expected = read_data("stub.pyi")
+            response = await client.post(
+                "/", data=source, headers={blackd.PYTHON_VARIANT_HEADER: "pyi"}
+            )
+            self.assertEqual(response.status, 200)
+            self.assertEqual(await response.text(), expected)
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_py36(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post(
+                "/",
+                data=(
+                    "def f(\n"
+                    "    and_has_a_bunch_of,\n"
+                    "    very_long_arguments_too,\n"
+                    "    and_lots_of_them_as_well_lol,\n"
+                    "    **and_very_long_keyword_arguments\n"
+                    "):\n"
+                    "    pass\n"
+                ),
+                headers={blackd.PYTHON_VARIANT_HEADER: "3.6"},
+            )
+            self.assertEqual(response.status, 200)
+            response = await client.post(
+                "/",
+                data=(
+                    "def f(\n"
+                    "    and_has_a_bunch_of,\n"
+                    "    very_long_arguments_too,\n"
+                    "    and_lots_of_them_as_well_lol,\n"
+                    "    **and_very_long_keyword_arguments\n"
+                    "):\n"
+                    "    pass\n"
+                ),
+                headers={blackd.PYTHON_VARIANT_HEADER: "3.5"},
+            )
+            self.assertEqual(response.status, 204)
+            response = await client.post(
+                "/",
+                data=(
+                    "def f(\n"
+                    "    and_has_a_bunch_of,\n"
+                    "    very_long_arguments_too,\n"
+                    "    and_lots_of_them_as_well_lol,\n"
+                    "    **and_very_long_keyword_arguments\n"
+                    "):\n"
+                    "    pass\n"
+                ),
+                headers={blackd.PYTHON_VARIANT_HEADER: "2"},
+            )
+            self.assertEqual(response.status, 204)
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_fast(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post("/", data=b"ur'hello'")
+            self.assertEqual(response.status, 500)
+            self.assertIn("failed to parse source file", await response.text())
+            response = await client.post(
+                "/", data=b"ur'hello'", headers={blackd.FAST_OR_SAFE_HEADER: "fast"}
+            )
+            self.assertEqual(response.status, 200)
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_line_length(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post(
+                "/", data=b'print("hello")\n', headers={blackd.LINE_LENGTH_HEADER: "7"}
+            )
+            self.assertEqual(response.status, 200)
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    @async_test
+    async def test_blackd_invalid_line_length(self) -> None:
+        app = blackd.make_app()
+        async with TestClient(TestServer(app)) as client:
+            response = await client.post(
+                "/",
+                data=b'print("hello")\n',
+                headers={blackd.LINE_LENGTH_HEADER: "NaN"},
+            )
+            self.assertEqual(response.status, 400)
+
+    @unittest.skipUnless(has_blackd_deps, "blackd's dependencies are not installed")
+    def test_blackd_main(self) -> None:
+        with patch("blackd.web.run_app"):
+            result = CliRunner().invoke(blackd.main, [])
+            if result.exception is not None:
+                raise result.exception
+            self.assertEqual(result.exit_code, 0)
+
 
 if __name__ == "__main__":
     unittest.main(module="test_black")
 
 if __name__ == "__main__":
     unittest.main(module="test_black")