blackd: a HTTP server for blackening (#460)

This commit is contained in:
Zsolt Dollenstein 2018-09-17 18:02:25 +01:00 committed by Łukasz Langa
parent 80500748a7
commit a82f186787
13 changed files with 536 additions and 46 deletions

View File

@ -1,10 +1,10 @@
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
- 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

View File

@ -4,12 +4,12 @@ language: python
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
- 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.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:

View File

@ -4,10 +4,12 @@ verify_ssl = true
name = "pypi"
[packages]
aiohttp = ">=3.3.2"
attrs = ">=17.4.0"
click = ">=6.5"
appdirs = "*"
toml = ">=0.9.4"
black = {editable = true, path = ".", extras = ["d"]}
[dev-packages]
pre-commit = "*"

164
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "83e7921ad3bcb0c1ee6c654ef6c2b737dd72629c2541f387ac186bedc55d33af"
"sha256": "1becc24ea9195b4295de408a96a019ad449aaee7da84555b6857bb2125749892"
},
"pipfile-spec": 6,
"requires": {},
@ -14,6 +14,34 @@
]
},
"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",
@ -22,6 +50,14 @@
"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",
@ -30,6 +66,20 @@
"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",
@ -38,12 +88,60 @@
"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"
},
"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": {
@ -78,10 +176,10 @@
},
"bleach": {
"hashes": [
"sha256:b8fa79e91f96c2c2cd9fd1f9eda906efb1b88b483048978ba62fef680e962b34",
"sha256:eb7386f632349d10d9ce9d4a838b134d4731571851149f9cc2c05a9a837a9a44"
"sha256:0ee95f6167129859c5dce9b1ca291ebdb5d8cd7e382ca0e237dfd0dad63f63d8",
"sha256:24754b9a7d530bf30ce7cbc805bc6cce785660b4a10ff3a43633728438c105ab"
],
"version": "==2.1.3"
"version": "==2.1.4"
},
"cached-property": {
"hashes": [
@ -92,10 +190,10 @@
},
"certifi": {
"hashes": [
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
],
"version": "==2018.4.16"
"version": "==2018.8.24"
},
"cffi": {
"hashes": [
@ -152,6 +250,7 @@
"hashes": [
"sha256:0186dccca79483e3405217993b83b914ba4559fe9a8396efc4eea56561b74061",
"sha256:1a625afc6f62da428df96ec325dc30866cc5781520cbd904ff4ec44cf018171c",
"sha256:207b7673ff4e177374c572feeae0e4ef33be620ec9171c08fd22e2b796e03e3d",
"sha256:275905bb371a99285c74931700db3f0c078e7603bed383e8cf1a09f3ee05a3de",
"sha256:50098f1c4950722521f0671e54139e0edc1837d63c990cf0f3d2c49607bb51a2",
"sha256:50ed116d0b60a07df0dc7b180c28569064b9d37d1578d4c9021cff04d725cb63",
@ -163,15 +262,19 @@
"sha256:811527e9b7280b136734ed6cb6845e5fbccaeaa132ddf45f0246cbe544016957",
"sha256:987b0e157f70c72a84f3c2f9ef2d7ab0f26c08f2bf326c12c087ff9eebcb3ff5",
"sha256:9fc6a2183d0a9b0974ec7cdcdad42bd78a3be674cc3e65f87dd694419b3b0ab7",
"sha256:a3d17ee4ae739fe16f7501a52255c2e287ac817cfd88565b9859f70520afffea",
"sha256:ba5b5488719c0f2ced0aa1986376f7baff1a1653a8eb5fdfcf3f84c7ce46ef8d",
"sha256:c573ea89dd95d41b6d8cf36799c34b6d5b1eac4aed0212dee0f0a11fb7b01e8f",
"sha256:c5f1b9e8592d2c448c44e6bc0d91224b16ea5f8293908b1561de1f6d2d0658b1",
"sha256:cbe581456357d8f0674d6a590b1aaf46c11d01dd0a23af147a51a798c3818034",
"sha256:cf219bec69e601fe27e3974b7307d2f06082ab385d42752738ad2eb630a47d65",
"sha256:cf5014eb214d814a83a7a47407272d5db10b719dbeaf4d3cfe5969309d0fcf4b",
"sha256:d08bad67fa18f7e8ff738c090628ee0cbf0505d74a991c848d6d04abfe67b697",
"sha256:d6f716d7b1182bf35862b5065112f933f43dd1aa4f8097c9bcfb246f71528a34",
"sha256:e08e479102627641c7cb4ece421c6ed4124820b1758765db32201136762282d9",
"sha256:e20ac21418af0298437d29599f7851915497ce9f2866bc8e86b084d8911ee061",
"sha256:e25f53c37e319241b9a412382140dffac98ca756ba8f360ac7ab5e30cad9670a",
"sha256:e8932bddf159064f04e946fbb64693753488de21586f20e840b3be51745c8c09",
"sha256:f20900f16377f2109783ae9348d34bc80530808439591c3d3df73d5c7ef1a00c"
],
"version": "==0.4.2"
@ -186,8 +289,11 @@
"hashes": [
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
"sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
"sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
"sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
@ -234,11 +340,11 @@
},
"flake8-bugbear": {
"hashes": [
"sha256:541746f0f3b2f1a8d7278e1d2d218df298996b60b02677708560db7c7e620e3b",
"sha256:5f14a99d458e29cb92be9079c970030e0dd398b2decb179d76d39a5266ea1578"
"sha256:07b6e769d7f4e168d590f7088eae40f6ddd9fa4952bed31602def65842682c83",
"sha256:0ccf56975f4db1d69dc1cf3598c99d768ebf95d0cad27d76087954aa399b515a"
],
"index": "pypi",
"version": "==18.2.0"
"version": "==18.8.0"
},
"flake8-mypy": {
"hashes": [
@ -263,10 +369,10 @@
},
"identify": {
"hashes": [
"sha256:0bab212939e3e83caf60788fac9bda5ea4a496f9147ed4746bb04f10ec6c0cbf",
"sha256:264f34cbd4002d5b2f4b323ae9e5776a16189363d82627f46570b9c703fea448"
"sha256:49845e70fc6b1ec3694ab930a2c558912d7de24548eebcd448f65567dc757c43",
"sha256:68daab16a3db364fa204591f97dc40bfffd1a7739f27788a4895b4d8fd3516e5"
],
"version": "==1.1.3"
"version": "==1.1.4"
},
"idna": {
"hashes": [
@ -312,9 +418,9 @@
},
"nodeenv": {
"hashes": [
"sha256:0611c726af1b252908646787f4d49811aa69cd92ec19644ded06ad9d3162f88e"
"sha256:aa040ab5189bae17d272175609010be6c5b589ec4b8dbd832cc50c9e9cb7496f"
],
"version": "==1.3.1"
"version": "==1.3.2"
},
"packaging": {
"hashes": [
@ -332,11 +438,11 @@
},
"pre-commit": {
"hashes": [
"sha256:2d57dd6b0c117ef8363233f256de8a3f26ee1c6e05ed96f9e2d9135ca5467d90",
"sha256:9807f29320547a8a13163c1977f6765e488a9349a01431ff4fbd196ff287b51c"
"sha256:c1472b0d73e27a5697a477f77a4973c708da4fc433cc89648e8612c8cd623b87",
"sha256:ec206de6fbcbd9381ff3169f9975571fb2efa99794c328518a5f0c06ae0b49c5"
],
"index": "pypi",
"version": "==1.10.3"
"version": "==1.10.5"
},
"pycodestyle": {
"hashes": [
@ -415,7 +521,6 @@
"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": {
@ -423,7 +528,6 @@
"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": {
@ -442,18 +546,18 @@
},
"sphinx": {
"hashes": [
"sha256:217ad9ece2156ed9f8af12b5d2c82a499ddf2c70a33c5f81864a08d8c67b9efc",
"sha256:a765c6db1e5b62aae857697cd4402a5c1a315a7b0854bbcd0fc8cdc524da5896"
"sha256:71531900af3f68625a29c4e00381bee8f85255219a3d500a3e255076a45b735e",
"sha256:a3defde5e17b5bc2aa21820674409287acc4d56bf8d009213d275e4b9d0d490d"
],
"index": "pypi",
"version": "==1.7.6"
"version": "==1.7.7"
},
"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": {
@ -465,11 +569,11 @@
},
"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": [
@ -512,7 +616,7 @@
"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": {
@ -520,7 +624,7 @@
"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": {

View File

@ -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)** |
**[blackd](#blackd)** |
**[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).
## 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
@ -850,8 +921,14 @@ More details can be found in [CONTRIBUTING](CONTRIBUTING.md).
### 18.8b0
* added `blackd`, see [its documentation](#blackd) for more info (#349)
* 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

View File

@ -79,15 +79,15 @@
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):
"""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):
@ -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>"
exc = ValueError(f"Cannot parse: {lineno}:{column}: {faulty_line}")
exc = InvalidInput(f"Cannot parse: {lineno}:{column}: {faulty_line}")
else:
raise exc from None

106
blackd.py Normal file
View File

@ -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()

1
docs/blackd.md Symbolic link
View File

@ -0,0 +1 @@
_build/generated/blackd.md

View File

@ -52,6 +52,7 @@ Contents
the_black_code_style
pyproject_toml
editor_integration
blackd
version_control_integration
ignoring_unmodified_files
contributing

View File

@ -29,3 +29,6 @@ check_untyped_defs=True
# No incremental mode
cache_dir=/dev/null
[mypy-aiohttp.*]
follow_imports=skip

View File

@ -62,6 +62,7 @@ attrs = "^17.4"
click = "^6.5"
toml = "^0.9.4"
appdirs = "^1.4"
aiohttp = "^3.4"
[tool.poetry.dev-dependencies]
Sphinx = "^1.7"

View File

@ -36,12 +36,13 @@ def get_version() -> str:
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"],
extras_require={"d": ["aiohttp>=3.3.2"]},
test_suite="tests.test_black",
classifiers=[
"Development Status :: 4 - Beta",
@ -56,5 +57,5 @@ def get_version() -> str:
"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]"]},
)

View File

@ -2,14 +2,24 @@
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 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
@ -18,6 +28,14 @@
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)
@ -25,6 +43,8 @@
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:
@ -79,6 +99,15 @@ def event_loop(close: bool) -> Iterator[None]:
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.
@ -824,7 +853,7 @@ def test_format_file_contents(self) -> None:
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")
@ -1289,6 +1318,171 @@ def test_shhh_click(self) -> None:
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")