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
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:
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 = "*"
{
"_meta": {
"hash": {
- "sha256": "83e7921ad3bcb0c1ee6c654ef6c2b737dd72629c2541f387ac186bedc55d33af"
+ "sha256": "1becc24ea9195b4295de408a96a019ad449aaee7da84555b6857bb2125749892"
},
"pipfile-spec": 6,
"requires": {},
]
},
"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",
"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",
"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",
"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": {
},
"bleach": {
"hashes": [
- "sha256:b8fa79e91f96c2c2cd9fd1f9eda906efb1b88b483048978ba62fef680e962b34",
- "sha256:eb7386f632349d10d9ce9d4a838b134d4731571851149f9cc2c05a9a837a9a44"
+ "sha256:0ee95f6167129859c5dce9b1ca291ebdb5d8cd7e382ca0e237dfd0dad63f63d8",
+ "sha256:24754b9a7d530bf30ce7cbc805bc6cce785660b4a10ff3a43633728438c105ab"
],
- "version": "==2.1.3"
+ "version": "==2.1.4"
},
"cached-property": {
"hashes": [
},
"certifi": {
"hashes": [
- "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
- "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
+ "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
+ "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
],
- "version": "==2018.4.16"
+ "version": "==2018.8.24"
},
"cffi": {
"hashes": [
"hashes": [
"sha256:0186dccca79483e3405217993b83b914ba4559fe9a8396efc4eea56561b74061",
"sha256:1a625afc6f62da428df96ec325dc30866cc5781520cbd904ff4ec44cf018171c",
+ "sha256:207b7673ff4e177374c572feeae0e4ef33be620ec9171c08fd22e2b796e03e3d",
"sha256:275905bb371a99285c74931700db3f0c078e7603bed383e8cf1a09f3ee05a3de",
"sha256:50098f1c4950722521f0671e54139e0edc1837d63c990cf0f3d2c49607bb51a2",
"sha256:50ed116d0b60a07df0dc7b180c28569064b9d37d1578d4c9021cff04d725cb63",
"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"
"hashes": [
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
+ "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
+ "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
+ "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
},
"flake8-bugbear": {
"hashes": [
- "sha256:541746f0f3b2f1a8d7278e1d2d218df298996b60b02677708560db7c7e620e3b",
- "sha256:5f14a99d458e29cb92be9079c970030e0dd398b2decb179d76d39a5266ea1578"
+ "sha256:07b6e769d7f4e168d590f7088eae40f6ddd9fa4952bed31602def65842682c83",
+ "sha256:0ccf56975f4db1d69dc1cf3598c99d768ebf95d0cad27d76087954aa399b515a"
],
"index": "pypi",
- "version": "==18.2.0"
+ "version": "==18.8.0"
},
"flake8-mypy": {
"hashes": [
},
"identify": {
"hashes": [
- "sha256:0bab212939e3e83caf60788fac9bda5ea4a496f9147ed4746bb04f10ec6c0cbf",
- "sha256:264f34cbd4002d5b2f4b323ae9e5776a16189363d82627f46570b9c703fea448"
+ "sha256:49845e70fc6b1ec3694ab930a2c558912d7de24548eebcd448f65567dc757c43",
+ "sha256:68daab16a3db364fa204591f97dc40bfffd1a7739f27788a4895b4d8fd3516e5"
],
- "version": "==1.1.3"
+ "version": "==1.1.4"
},
"idna": {
"hashes": [
},
"nodeenv": {
"hashes": [
- "sha256:0611c726af1b252908646787f4d49811aa69cd92ec19644ded06ad9d3162f88e"
+ "sha256:aa040ab5189bae17d272175609010be6c5b589ec4b8dbd832cc50c9e9cb7496f"
],
- "version": "==1.3.1"
+ "version": "==1.3.2"
},
"packaging": {
"hashes": [
},
"pre-commit": {
"hashes": [
- "sha256:2d57dd6b0c117ef8363233f256de8a3f26ee1c6e05ed96f9e2d9135ca5467d90",
- "sha256:9807f29320547a8a13163c1977f6765e488a9349a01431ff4fbd196ff287b51c"
+ "sha256:c1472b0d73e27a5697a477f77a4973c708da4fc433cc89648e8612c8cd623b87",
+ "sha256:ec206de6fbcbd9381ff3169f9975571fb2efa99794c328518a5f0c06ae0b49c5"
],
"index": "pypi",
- "version": "==1.10.3"
+ "version": "==1.10.5"
},
"pycodestyle": {
"hashes": [
"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": {
"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": {
},
"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": {
},
"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": [
"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": {
"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": {
**[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)** |
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
### 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
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):
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
--- /dev/null
+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()
--- /dev/null
+_build/generated/blackd.md
\ No newline at end of file
the_black_code_style
pyproject_toml
editor_integration
+ blackd
version_control_integration
ignoring_unmodified_files
contributing
# No incremental mode
cache_dir=/dev/null
+
+[mypy-aiohttp.*]
+follow_imports=skip
click = "^6.5"
toml = "^0.9.4"
appdirs = "^1.4"
+aiohttp = "^3.4"
[tool.poetry.dev-dependencies]
Sphinx = "^1.7"
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",
"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]"]},
)
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
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)
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:
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.
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")
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")