From 23a7e4ef99a604ac5ef27a1bef52ad1c19ffc3e0 Mon Sep 17 00:00:00 2001 From: Taylor Jackle Spriggs Date: Mon, 31 Aug 2020 14:05:00 -0700 Subject: [PATCH] Update to 20.8b1 (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Find project root correctly (#1518) Ensure root dir is a common parent of all inputs Fixes #1493 * Add pip install from GitHub command to README.md (#1529) * Add pip install from GitHub command to README.md * Make it prettier ... * Mozilla uses black too (#1531) * add Quora to orgs that use Black (#1532) * ISSUE 1533: Fix --config argument description (#1534) Change --config argument description to "Read configuration from FILE." The "--config FILE Read configuration from FILE path" * Spelling fix in CONTRIBUTING.md (#1547) * Ensure path for finding root is absolute (#1550) As Path.resolve() is buggy on windows (see https://bugs.python.org/issue38671) an absolute path is ensured by prepending the Path.cwd() * Update curl command to use stable branch (#1543) * pre-commit: show diff on failure on CI (#1552) * pre-commit: --show-diff-on-failure * pre-commit: --show-diff-on-failure * docs: Improve pre-commit use (#1551) Stable tag wasn't available and crashed when attempting to set initial pre-commit. Also the python version needs to be installed so it would be better to use the generic "python3" command. * Update to accomodate isort 5 release changes. (#1559) Isort 5 introduced profiles and ensure_newline_before_comments options. Either needs to be added to work correctly with black. Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> * fix spelling (#1567) Co-authored-by: Hugo van Kemenade * Remove slow assertion (#1592) Partial fix for #1581 This assertion produces behavior quadratic in the number of leaves in a line, which is making Black extremely slow on files with very long expressions. On my benchmark file this change makes Black 10x faster. * Add the direnv base directory to the default excludes (#1564) Co-authored-by: Chris Rose * Make --exclude only apply to recursively found files (#1591) Ever since --force-exclude was added, --exclude started to touch files that were given to Black through the CLI too. This is not documented behaviour and neither expected as --exclude and --force-exclude now behave the same! Before this commit, get_sources() when encountering a file that was passed explicitly through the CLI would pass a single Path object list to gen_python_files(). This causes bad behaviour since that function doesn't treat the exclude and force_exclude regexes differently. Which is fine for recursively found files, but *not* for files given through the CLI. Now when get_sources() iterates through srcs and encounters a file, it checks if the force_exclude regex matches, if not, then the file will be added to the computed sources set. A new function had to be created since before you can do regex matching, the path must be normalized. The full process of normalizing the path is somewhat long as there is special error handling. I didn't want to duplicate this logic in get_sources() and gen_python_files() so that's why there is a new helper function. * in verbose mode, print stack trace (#1594) Make Black failures easier to debug * fix some docstring crashes (#1593) Allow removing some trailing whitespace * don't strip brackets before lsqb (#1575) (#1590) if the string contains a PERCENT, it's not safe to remove brackets that follow and operator with the same or higher precedence than PERCENT * fix unary op detection (#1600) * Fix inline code style in README (#1608) Refer to `pyproject.toml` in HTML section of README with HTML code tags * Update all dependencies to latest versions * Disable string splitting/merging by default (#1609) * put experimental string stuff behind a flag * update tests * don't need an output section if it's the same as the input * Primer: Expect no formatting changes in attrs, hypothesis and poetry with --experimental-string-processing off Co-authored-by: Hugo van Kemenade * Upgrade docs to Sphinx 3+ and add doc build test (#1613) * Upgrade docs to Sphinx 3+ * Fix all the warnings... - Fixed bad docstrings - Fixed bad fenced code blocks in documentation - Blocklisted some sections from being generated from the README - Added missing documentation to index.rst - Fixed an invalid autofunction directive in reference/reference_functions.rst - Pin another documentation dependency * Add documentation build test * Reset trailing comma handling * Re-implement magic trailing comma handling: - when a trailing comma is specified in any bracket pair, that signals to Black that this bracket pair needs to be always exploded, e.g. presented as "one item per line"; - this causes some changes to previously formatted code that erroneously left trailing commas embedded into single-line expressions; - internally, Black needs to be able to identify trailing commas that it put itself compared to pre-existing trailing commas. We do this by using/abusing lib2to3's `was_checked` attribute. It's True for internally generated trailing commas and False for pre-existing ones (in fact, for all pre-existing leaves and nodes). Fixes #1288 * Reformat docs/conf.py according to the new style * Open file explicitly with UTF-8 so it works on Windows, too * Mark Primer projects that will change formatting * Require Sphinx 3 * Use properly renamed function name in docs * Fix dealing with generated files in docs * Property-based fuzz test * Update the changelog * Make doc generation a little smarter, update doc sections * Add more trailing comma test variants * Run trailing comma tests with TargetVersion.PY38 * Address pre-existing trailing commas when not in the rightmost bracket pair This required some hackery. Long story short, we need to reuse the ability to omit rightmost bracket pairs (which glues them together and splits on something else instead), for use with pre-existing trailing commas. This form of user-controlled formatting is brittle so we have to be careful not to cause a scenario where Black first formats code without trailing commas in one way, and then looks at the same file with pre-existing trailing commas (that it itself put on the previous run) and decides to format the code again. One particular ugly edge case here is handling of optional parentheses. In particular, the long-standing `line_length=1` hack got in the way of pre-existing trailing commas and had to be removed. Instead, a more intelligent but costly solution was put in place: a "second opinion" if the formatting that omits optional parentheses ended up causing lines to be too long. Again, for efficiency purposes, Black reuses Leaf objects from blib2to3 and modifies them in place, which was invalid for having two separate formattings. Line cloning was used to mitigate this. Fixes #1619 * Improve docstring re-indentation handling This addresses a few crashers, namely: * producing non-equivalent code due to mangling escaped newlines, * invalid hugging quote characters in the docstring body to the docstring outer triple quotes (causing a quadruple quote which is a syntax error), * lack of handling for docstrings that start on the same line as the `def`, and * invalid stripping of outer triple quotes when the docstring contained a string prefix. As a bonus, tests now also run when string normalization is disabled. * Add links regarding Spotless integration for gradle/maven users (#1622) Co-authored-by: Łukasz Langa * Primer update config - enable pytest (#1626) Reformatted projects I have acceess to: - aioexabgp - bandersnatch - flake8-bugbear ``` -- primer results 📊 -- 13 / 16 succeeded (81.25%) ✅ 0 / 16 FAILED (0.0%) 💩 - 3 projects disabled by config - 0 projects skipped due to Python version - 0 skipped due to long checkout ``` * Also re-enable pytest ``` -- primer results 📊 -- 14 / 16 succeeded (87.5%) ✅ 0 / 16 FAILED (0.0%) 💩 - 2 projects disabled by config - 0 projects skipped due to Python version - 0 skipped due to long checkout real 2m26.207s user 17m55.404s sys 0m43.061s ``` * v20.8b0 * Treat all trailing commas as pre-existing, as they effectively are On a second pass of Black on the same file, inserted trailing commas are now pre-existing. Doesn't make sense to differentiate between the passes then. * Include mode information for unstable formattings * Add expected failure tests with the unstable formattings * Make dependency on Click 7.0, regex 2020.1.8, and toml 0.10.1 explicit * v20.8b1 Co-authored-by: Lihu Ben-Ezri-Ravin Co-authored-by: Cooper Lees Co-authored-by: Sylvestre Ledru Co-authored-by: Jelle Zijlstra Co-authored-by: Olexiy Co-authored-by: jtpavlock Co-authored-by: dhaug-op <56020126+dhaug-op@users.noreply.github.com> Co-authored-by: Steven Maude Co-authored-by: Hugo van Kemenade Co-authored-by: Vinicius Gubiani Ferreira Co-authored-by: Maximilian Cosmo Sitter <48606431+mcsitter@users.noreply.github.com> Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> Co-authored-by: Kaligule Co-authored-by: Chris Rose Co-authored-by: Chris Rose Co-authored-by: David Szotten Co-authored-by: Daanyaal Syed Co-authored-by: Łukasz Langa Co-authored-by: Zac-HD Co-authored-by: Ned Twigg --- .github/workflows/doc.yml | 36 + .github/workflows/fuzz.yml | 31 + .github/workflows/lint.yml | 2 +- .gitignore | 1 + .travis.yml | 2 +- CHANGES.md | 51 +- CONTRIBUTING.md | 2 +- Pipfile | 10 +- Pipfile.lock | 647 +++++++--------- README.md | 26 +- docs/authors.md | 184 +++++ docs/black_primer.md | 6 +- docs/change_log.md | 503 ++++++++++++ docs/compatible_configs.md | 17 +- docs/conf.py | 66 +- docs/contributing_to_black.md | 70 ++ docs/editor_integration.md | 10 +- docs/github_actions.md | 19 + docs/ignoring_unmodified_files.md | 23 + docs/index.rst | 3 +- docs/installation_and_usage.md | 179 +++++ docs/pyproject_toml.md | 88 +++ docs/reference/reference_functions.rst | 4 +- docs/requirements.txt | 5 +- docs/show_your_style.md | 19 + docs/the_black_code_style.md | 13 +- docs/version_control_integration.md | 28 + fuzz.py | 59 ++ gallery/gallery.py | 5 +- setup.py | 5 +- src/black/__init__.py | 727 +++++++++++------- src/black_primer/primer.json | 10 +- src/blib2to3/pgen2/driver.py | 2 +- tests/data/cantfit.py | 12 +- tests/data/collections.py | 35 +- tests/data/comments2.py | 8 +- tests/data/comments7.py | 151 +++- tests/data/composition_no_trailing_comma.py | 367 +++++++++ tests/data/docstring.py | 77 +- tests/data/expression.diff | 27 +- tests/data/expression.py | 22 +- tests/data/fmtonoff4.py | 7 +- tests/data/function.py | 5 +- tests/data/function2.py | 5 +- tests/data/function_trailing_comma.py | 79 +- tests/data/import_spacing.py | 8 +- tests/data/long_strings.py | 32 +- tests/data/long_strings__regression.py | 12 +- tests/data/long_strings_flag_disabled.py | 289 +++++++ tests/data/percent_precedence.py | 41 + tests/data/trailing_comma_optional_parens1.py | 3 + tests/data/trailing_comma_optional_parens2.py | 3 + tests/data/trailing_comma_optional_parens3.py | 8 + tests/test_black.py | 357 ++++++--- 54 files changed, 3573 insertions(+), 828 deletions(-) create mode 100644 .github/workflows/doc.yml create mode 100644 .github/workflows/fuzz.yml create mode 100644 docs/authors.md create mode 100644 docs/change_log.md create mode 100644 docs/contributing_to_black.md create mode 100644 docs/github_actions.md create mode 100644 docs/ignoring_unmodified_files.md create mode 100644 docs/installation_and_usage.md create mode 100644 docs/pyproject_toml.md create mode 100644 docs/show_your_style.md create mode 100644 docs/version_control_integration.md create mode 100644 fuzz.py create mode 100644 tests/data/composition_no_trailing_comma.py create mode 100644 tests/data/long_strings_flag_disabled.py create mode 100644 tests/data/percent_precedence.py create mode 100644 tests/data/trailing_comma_optional_parens1.py create mode 100644 tests/data/trailing_comma_optional_parens2.py create mode 100644 tests/data/trailing_comma_optional_parens3.py diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 00000000000..6023a02a7f7 --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,36 @@ +name: Documentation Build + +on: + push: + paths: + - "docs/**" + - "README.md" + - "CHANGES.md" + - "CONTRIBUTING.md" + pull_request: + paths: + - "docs/**" + - "README.md" + - "CHANGES.md" + - "CONTRIBUTING.md" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install -e "." + python -m pip install -r "docs/requirements.txt" + + - name: Build documentation + run: sphinx-build -a -b html -W docs/ docs/_build/ diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 00000000000..92caa0fd5c1 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,31 @@ +name: Fuzz + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade coverage + python -m pip install --upgrade hypothesmith + python -m pip install -e ".[d]" + + - name: Run fuzz tests + run: | + coverage run fuzz.py + coverage report diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cb8c534c9ac..fa7286eec1f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,4 +24,4 @@ jobs: python -m pip install -e '.[d]' - name: Lint - run: pre-commit run --all-files + run: pre-commit run --all-files --show-diff-on-failure diff --git a/.gitignore b/.gitignore index 509797e65c4..6b94cacd183 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ src/_black_version.py .eggs .dmypy.json *.swp +.hypothesis/ diff --git a/.travis.yml b/.travis.yml index b2b127cfeb0..86cf24df51b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ matrix: - name: "lint" python: 3.7 env: - - TEST_CMD="pre-commit run --all-files" + - TEST_CMD="pre-commit run --all-files --show-diff-on-failure" - name: "3.6" python: 3.6 - name: "3.7" diff --git a/CHANGES.md b/CHANGES.md index 6d418b9bec8..11341779f58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,18 +1,57 @@ ## Change Log -### Unreleased +### 20.8b1 + +#### _Packaging_ + +- explicitly depend on Click 7.1.2 or newer as `Black` no longer works with versions + older than 7.0 + +### 20.8b0 #### _Black_ -- reindent docstrings when reindenting code around it (#1053) +- re-implemented support for explicit trailing commas: now it works consistently within + any bracket pair, including nested structures (#1288 and duplicates) + +- `Black` now reindents docstrings when reindenting code around it (#1053) + +- `Black` now shows colored diffs (#1266) + +- `Black` is now packaged using 'py3' tagged wheels (#1388) + +- `Black` now supports Python 3.8 code, e.g. star expressions in return statements + (#1121) + +- `Black` no longer normalizes capital R-string prefixes as those have a + community-accepted meaning (#1244) + +- `Black` now uses exit code 2 when specified configuration file doesn't exit (#1361) + +- `Black` now works on AWS Lambda (#1141) + +- added `--force-exclude` argument (#1032) + +- removed deprecated `--py36` option (#1236) + +- fixed `--diff` output when EOF is encountered (#526) + +- fixed `# fmt: off` handling around decorators (#560) + +- fixed unstable formatting with some `# type: ignore` comments (#1113) -- show colored diffs (#1266) +- fixed invalid removal on organizing brackets followed by indexing (#1575) -- move to 'py3' tagged wheels (#1388) +- introduced `black-primer`, a CI tool that allows us to run regression tests against + existing open source users of Black (#1402) -- remove deprecated `--py36` option (#1236) +- introduced property-based fuzzing to our test suite based on Hypothesis and + Hypothersmith (#1566) -- add `--force-exclude` argument (#1032) +- implemented experimental and disabled by default long string rewrapping (#1132), + hidden under a `--experimental-string-processing` flag while it's being worked on; + this is an undocumented and unsupported feature, you lose Internet points for + depending on it (#1609) #### Vim plugin diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 525cb9c183d..0687aaeee52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,7 @@ $ black-primer [-k -w /tmp/black_test_repos] ## black-primer `black-primer` is used by CI to pull down well-known _Black_ formatted projects and see -if we get soure code changes. It will error on formatting changes or errors. Please run +if we get source code changes. It will error on formatting changes or errors. Please run before pushing your PR to see if you get the actions you would expect from _Black_ with your PR. You may need to change [primer.json](https://github.com/psf/black/blob/master/src/black_primer/primer.json) diff --git a/Pipfile b/Pipfile index 3afe3bb9efd..44f57f6773c 100644 --- a/Pipfile +++ b/Pipfile @@ -4,13 +4,13 @@ url = "https://pypi.python.org/simple" verify_ssl = true [dev-packages] -Sphinx = "*" +Sphinx = ">=3.1.2" coverage = "*" docutils = "==0.15" # not a direct dependency, see https://github.com/pypa/pipenv/issues/3865 flake8 = "*" flake8-bugbear = "*" flake8-mypy = "*" -mypy = ">=0.740" +mypy = ">=0.782" pre-commit = "*" readme_renderer = "*" recommonmark = "*" @@ -18,16 +18,16 @@ setuptools = ">=39.2.0" setuptools-scm = "*" twine = ">=1.11.0" wheel = ">=0.31.1" -black = {editable = true,extras = ["d"],path = "."} +black = {editable = true, extras = ["d"], path = "."} [packages] aiohttp = ">=3.3.2" aiohttp-cors = "*" appdirs = "*" -click = ">=6.5" +click = ">=7.0" mypy_extensions = ">=0.4.3" pathspec = ">=0.6" -regex = ">=2019.8" +regex = ">=2020.1.8" toml = ">=0.10.1" typed-ast = "==1.4.0" typing_extensions = ">=3.7.4" diff --git a/Pipfile.lock b/Pipfile.lock index b0da4743793..32b8012ff0e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4a6956c7c81b496d3fd7a4e3395b332d4dc9a5bed468e36e729a4039c739ad2d" + "sha256": "61d09a6b8a8c310becd5e108ed08e0eeae50c7323c08c8040367abded0cb1031" }, "pipfile-spec": 6, "requires": {}, @@ -42,25 +42,27 @@ }, "appdirs": { "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" ], "index": "pypi", - "version": "==1.4.3" + "version": "==1.4.4" }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", + "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.1.0" }, "black": { "editable": true, @@ -78,11 +80,11 @@ }, "click": { "hashes": [ - "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", - "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], "index": "pypi", - "version": "==7.1.1" + "version": "==7.1.2" }, "dataclasses": { "hashes": [ @@ -90,36 +92,40 @@ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84" ], "index": "pypi", - "version": "==0.6" + "python_version <": "3.7", + "version": "==0.6", + "version >": "0.6" }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "multidict": { "hashes": [ - "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", - "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", - "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", - "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", - "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", - "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", - "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", - "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", - "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", - "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", - "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", - "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", - "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", - "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", - "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", - "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", - "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" - ], - "version": "==4.7.5" + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "markers": "python_version >= '3.5'", + "version": "==4.7.6" }, "mypy-extensions": { "hashes": [ @@ -131,38 +137,38 @@ }, "pathspec": { "hashes": [ - "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", - "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" ], "index": "pypi", - "version": "==0.7.0" + "version": "==0.8.0" }, "regex": { "hashes": [ - "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431", - "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242", - "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1", - "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d", - "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045", - "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b", - "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400", - "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa", - "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0", - "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69", - "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74", - "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb", - "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26", - "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5", - "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2", - "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce", - "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab", - "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e", - "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70", - "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc", - "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0" - ], - "index": "pypi", - "version": "==2020.2.20" + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "index": "pypi", + "version": "==2020.7.14" }, "setuptools-scm": { "hashes": [ @@ -180,10 +186,10 @@ }, "toml": { "hashes": [ - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "index": "pypi", "version": "==0.10.1" @@ -216,34 +222,35 @@ }, "typing-extensions": { "hashes": [ - "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", - "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", - "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "version": "==3.7.4.1" + "version": "==3.7.4.3" }, "yarl": { "hashes": [ - "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", - "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", - "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", - "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", - "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", - "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", - "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", - "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", - "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", - "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", - "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", - "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", - "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", - "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", - "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", - "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", - "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" - ], - "version": "==1.4.2" + "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", + "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", + "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", + "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", + "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", + "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", + "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", + "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", + "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", + "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", + "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", + "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", + "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", + "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", + "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", + "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", + "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + ], + "markers": "python_version >= '3.5'", + "version": "==1.5.1" } }, "develop": { @@ -282,25 +289,27 @@ }, "appdirs": { "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" ], "index": "pypi", - "version": "==1.4.3" + "version": "==1.4.4" }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", + "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.1.0" }, "babel": { "hashes": [ @@ -327,51 +336,18 @@ }, "certifi": { "hashes": [ - "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1", - "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc" - ], - "version": "==2020.4.5.2" - }, - "cffi": { - "hashes": [ - "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", - "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", - "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", - "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", - "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", - "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", - "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", - "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", - "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", - "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", - "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", - "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", - "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", - "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", - "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", - "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", - "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", - "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", - "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", - "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", - "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", - "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", - "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", - "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", - "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", - "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", - "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", - "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" - ], - "version": "==1.14.0" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" }, "cfgv": { "hashes": [ - "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", - "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" + "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", + "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], "markers": "python_full_version >= '3.6.1'", - "version": "==3.1.0" + "version": "==3.2.0" }, "chardet": { "hashes": [ @@ -382,11 +358,19 @@ }, "click": { "hashes": [ - "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", - "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], "index": "pypi", - "version": "==7.1.1" + "version": "==7.1.2" + }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.4.3" }, "commonmark": { "hashes": [ @@ -397,71 +381,50 @@ }, "coverage": { "hashes": [ - "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0", - "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30", - "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b", - "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0", - "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823", - "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe", - "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037", - "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6", - "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31", - "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd", - "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892", - "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1", - "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78", - "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac", - "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006", - "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014", - "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2", - "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7", - "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8", - "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7", - "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9", - "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1", - "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307", - "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a", - "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435", - "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0", - "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5", - "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441", - "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732", - "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de", - "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1" - ], - "index": "pypi", - "version": "==5.0.4" - }, - "cryptography": { - "hashes": [ - "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6", - "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b", - "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5", - "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf", - "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e", - "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b", - "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae", - "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b", - "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0", - "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b", - "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d", - "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229", - "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3", - "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365", - "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55", - "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270", - "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e", - "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785", - "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.9.2" + "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", + "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", + "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", + "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", + "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", + "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", + "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", + "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", + "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", + "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", + "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", + "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", + "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", + "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", + "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", + "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", + "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", + "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", + "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", + "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", + "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", + "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", + "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", + "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", + "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", + "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", + "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", + "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", + "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", + "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", + "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", + "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", + "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", + "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" + ], + "index": "pypi", + "version": "==5.2.1" }, "distlib": { "hashes": [ - "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" + "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", + "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" ], - "version": "==0.3.0" + "version": "==0.3.1" }, "docutils": { "hashes": [ @@ -472,13 +435,6 @@ "index": "pypi", "version": "==0.15" }, - "entrypoints": { - "hashes": [ - "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", - "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" - ], - "version": "==0.3" - }, "filelock": { "hashes": [ "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", @@ -488,11 +444,11 @@ }, "flake8": { "hashes": [ - "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", - "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" + "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", + "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" ], "index": "pypi", - "version": "==3.7.9" + "version": "==3.8.3" }, "flake8-bugbear": { "hashes": [ @@ -512,18 +468,19 @@ }, "identify": { "hashes": [ - "sha256:249ebc7e2066d6393d27c1b1be3b70433f824a120b1d8274d362f1eb419e3b52", - "sha256:781fd3401f5d2b17b22a8b18b493a48d5d948e3330634e82742e23f9c20234ef" + "sha256:9f5fcf22b665eaece583bd395b103c2769772a0f646ffabb5b1f155901b07de2", + "sha256:b1aa2e05863dc80242610d46a7b49105e2eafe00ef0c8ff311c1828680760c76" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.19" + "version": "==1.4.29" }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "imagesize": { "hashes": [ @@ -533,14 +490,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, - "jeepney": { - "hashes": [ - "sha256:3479b861cc2b6407de5188695fa1a8d57e5072d7059322469b62628869b8e36e", - "sha256:d6c6b49683446d2407d2fe3acb7a368a77ff063f9182fe427da15d622adc24cf" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.4.3" - }, "jinja2": { "hashes": [ "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", @@ -551,11 +500,11 @@ }, "keyring": { "hashes": [ - "sha256:3401234209015144a5d75701e71cb47239e552b0882313e9f51e8976f9e27843", - "sha256:c53e0e5ccde3ad34284a40ce7976b5b3a3d6de70344c3f8ee44364cc340976ec" + "sha256:182f94fc0381546489e3e4d90384a8c1d43cc09ffe2eb4a826e7312df6e1be7c", + "sha256:cd4d486803d55bdb13e2d453eb61dbbc984773e4f2b98a455aa85b1f4bc421e4" ], "markers": "python_version >= '3.6'", - "version": "==21.2.1" + "version": "==21.3.1" }, "markupsafe": { "hashes": [ @@ -605,45 +554,46 @@ }, "multidict": { "hashes": [ - "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1", - "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35", - "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928", - "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969", - "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e", - "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78", - "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1", - "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136", - "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8", - "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2", - "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e", - "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4", - "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5", - "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd", - "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab", - "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20", - "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3" - ], - "version": "==4.7.5" + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "markers": "python_version >= '3.5'", + "version": "==4.7.6" }, "mypy": { "hashes": [ - "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2", - "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1", - "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164", - "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761", - "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce", - "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27", - "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754", - "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae", - "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9", - "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600", - "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65", - "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8", - "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913", - "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3" + "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c", + "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86", + "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b", + "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd", + "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc", + "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea", + "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e", + "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308", + "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406", + "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d", + "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707", + "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d", + "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c", + "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a" ], "index": "pypi", - "version": "==0.770" + "version": "==0.782" }, "mypy-extensions": { "hashes": [ @@ -655,9 +605,10 @@ }, "nodeenv": { "hashes": [ - "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc" + "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9", + "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c" ], - "version": "==1.4.0" + "version": "==1.5.0" }, "packaging": { "hashes": [ @@ -669,11 +620,11 @@ }, "pathspec": { "hashes": [ - "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", - "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" ], "index": "pypi", - "version": "==0.7.0" + "version": "==0.8.0" }, "pkginfo": { "hashes": [ @@ -684,11 +635,11 @@ }, "pre-commit": { "hashes": [ - "sha256:487c675916e6f99d355ec5595ad77b325689d423ef4839db1ed2f02f639c9522", - "sha256:c0aa11bce04a7b46c5544723aedf4e81a4d5f64ad1205a30a9ea12d5e81969e1" + "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", + "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.7.1" }, "pycodestyle": { "hashes": [ @@ -698,14 +649,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20" - }, "pyflakes": { "hashes": [ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", @@ -755,11 +698,11 @@ }, "readme-renderer": { "hashes": [ - "sha256:1b6d8dd1673a0b293766b4106af766b6eff3654605f9c4f239e65de6076bc222", - "sha256:e67d64242f0174a63c3b727801a2fff4c1f38ebe5d71d95ff7ece081945a6cd4" + "sha256:cbe9db71defedd2428a1589cdc545f9bd98e59297449f69d721ef8f1cfced68d", + "sha256:cc4957a803106e820d05d14f71033092537a22daa4f406dfbdd61177e0936376" ], "index": "pypi", - "version": "==25.0" + "version": "==26.0" }, "recommonmark": { "hashes": [ @@ -771,38 +714,38 @@ }, "regex": { "hashes": [ - "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431", - "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242", - "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1", - "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d", - "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045", - "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b", - "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400", - "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa", - "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0", - "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69", - "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74", - "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb", - "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26", - "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5", - "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2", - "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce", - "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab", - "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e", - "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70", - "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc", - "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0" - ], - "index": "pypi", - "version": "==2020.2.20" + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "index": "pypi", + "version": "==2020.7.14" }, "requests": { "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.23.0" + "version": "==2.24.0" }, "requests-toolbelt": { "hashes": [ @@ -811,13 +754,12 @@ ], "version": "==0.9.1" }, - "secretstorage": { + "rfc3986": { "hashes": [ - "sha256:15da8a989b65498e29be338b3b279965f1b8f09b9668bd8010da183024c8bff6", - "sha256:b5ec909dde94d4ae2fa26af7c089036997030f0cf0a5cb372b4cccabd81c143b" + "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", + "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" ], - "markers": "sys_platform == 'linux'", - "version": "==3.1.2" + "version": "==1.4.0" }, "setuptools-scm": { "hashes": [ @@ -850,11 +792,11 @@ }, "sphinx": { "hashes": [ - "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66", - "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb" + "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8", + "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0" ], "index": "pypi", - "version": "==2.4.4" + "version": "==3.2.1" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -906,29 +848,29 @@ }, "toml": { "hashes": [ - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88", - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], "index": "pypi", "version": "==0.10.1" }, "tqdm": { "hashes": [ - "sha256:07c06493f1403c1380b630ae3dcbe5ae62abcf369a93bbc052502279f189ab8c", - "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f" + "sha256:1a336d2b829be50e46b84668691e0a2719f26c97c62846298dd5ae2937e4d5cf", + "sha256:564d632ea2b9cb52979f7956e093e831c28d441c11751682f84c86fc46e4fd21" ], "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.46.1" + "version": "==4.48.2" }, "twine": { "hashes": [ - "sha256:c1af8ca391e43b0a06bbc155f7f67db0bf0d19d284bfc88d1675da497a946124", - "sha256:d561a5e511f70275e5a485a6275ff61851c16ffcb3a95a602189161112d9f160" + "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab", + "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472" ], "index": "pypi", - "version": "==3.1.1" + "version": "==3.2.0" }, "typed-ast": { "hashes": [ @@ -958,28 +900,28 @@ }, "typing-extensions": { "hashes": [ - "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", - "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", - "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], "index": "pypi", - "version": "==3.7.4.1" + "version": "==3.7.4.3" }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.25.9" + "version": "==1.25.10" }, "virtualenv": { "hashes": [ - "sha256:5102fbf1ec57e80671ef40ed98a84e980a71194cedf30c87c2b25c3a9e0b0107", - "sha256:ccfb8e1e05a1174f7bd4c163700277ba730496094fe1a58bea9d4ac140a207c8" + "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", + "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.23" + "version": "==20.0.31" }, "webencodings": { "hashes": [ @@ -990,33 +932,34 @@ }, "wheel": { "hashes": [ - "sha256:8788e9155fe14f54164c1b9eb0a319d98ef02c160725587ad60f14ddc57b6f96", - "sha256:df277cb51e61359aba502208d680f90c0493adec6f0e848af94948778aed386e" + "sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2", + "sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f" ], "index": "pypi", - "version": "==0.34.2" + "version": "==0.35.1" }, "yarl": { "hashes": [ - "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", - "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", - "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", - "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", - "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", - "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", - "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", - "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", - "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", - "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", - "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", - "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", - "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", - "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", - "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", - "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", - "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" - ], - "version": "==1.4.2" + "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", + "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", + "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", + "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", + "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", + "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", + "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", + "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", + "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", + "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", + "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", + "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", + "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", + "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", + "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", + "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", + "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + ], + "markers": "python_version >= '3.5'", + "version": "==1.5.1" } } } diff --git a/README.md b/README.md index beed8ba4943..20f6fa420b2 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ _Contents:_ **[Installation and usage](#installation-and-usage)** | _Black_ can be installed by running `pip install black`. It requires Python 3.6.0+ to run but you can reformat Python 2 code with it, too. +#### Install from GitHub + +If you can't wait for the latest _hotness_ and want to install from GitHub, use: + +`pip install git+git://github.com/psf/black` + ### Usage To get started right away with sensible defaults: @@ -137,7 +143,7 @@ Options: --exclude=. --version Show the version and exit. - --config FILE Read configuration from PATH. + --config FILE Read configuration from FILE path. -h, --help Show this message and exit. ``` @@ -287,7 +293,7 @@ the equivalent of r-strings in Python. Multiline strings are treated as verbose expressions by Black. Use `[ ]` to denote a significant space character.
-Example `pyproject.toml` +Example pyproject.toml ```toml [tool.black] @@ -348,8 +354,8 @@ rolling. ## black-primer -`black-primer` is a tool built for CI (and huumans) to have _Black_ `--check` a number -of (configured in `primer.json`) Git accessible projects in parallel. +`black-primer` is a tool built for CI (and humans) to have _Black_ `--check` a number of +(configured in `primer.json`) Git accessible projects in parallel. [black_primer](https://github.com/psf/black/blob/master/docs/black_primer.md) has more information regarding its usage and configuration. @@ -364,10 +370,10 @@ Use [pre-commit](https://pre-commit.com/). Once you ```yaml repos: - repo: https://github.com/psf/black - rev: stable + rev: 19.10b0 # Replace by any tag/version: https://github.com/psf/black/tags hooks: - id: black - language_version: python3.6 + language_version: python3 # Should be a command that runs python3.6+ ``` Then run `pre-commit install` and you're ready to go. @@ -429,7 +435,7 @@ code style: pytest, tox, Pyramid, Django Channels, Hypothesis, attrs, SQLAlchemy Poetry, PyPA applications (Warehouse, Bandersnatch, Pipenv, virtualenv), pandas, Pillow, every Datadog Agent Integration, Home Assistant. -The following organizations use _Black_: Facebook, Dropbox. +The following organizations use _Black_: Facebook, Dropbox, Mozilla, Quora. Are we missing anyone? Let us know. @@ -458,7 +464,7 @@ Twisted and CPython: Use the badge in your project's README.md: -```markdown +```md [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) ``` @@ -542,6 +548,7 @@ Multiple contributions by: - [Christian Clauss](mailto:cclauss@bluewin.ch) - [Christian Heimes](mailto:christian@python.org) - [Chuck Wooters](mailto:chuck.wooters@microsoft.com) +- [Chris Rose](mailto:offline@offby1.net) - Codey Oxley - [Cong](mailto:congusbongus@gmail.com) - [Cooper Ry Lees](mailto:me@cooperlees.com) @@ -618,7 +625,7 @@ Multiple contributions by: - [Miroslav Shubernetskiy](mailto:miroslav@miki725.com) - MomIsBestFriend - [Nathan Goldbaum](mailto:ngoldbau@illinois.edu) -- [Nathan Hunt](mailtoneighthan.hunt@gmail.com) +- [Nathan Hunt](mailto:neighthan.hunt@gmail.com) - [Neraste](mailto:neraste.herr10@gmail.com) - [Nikolaus Waxweiler](mailto:madigens@gmail.com) - [Ofek Lev](mailto:ofekmeister@gmail.com) @@ -677,3 +684,4 @@ Multiple contributions by: - Yazdan - [Yngve Høiseth](mailto:yngve@hoiseth.net) - [Yurii Karabas](mailto:1998uriyyo@gmail.com) +- [Zac Hatfield-Dodds](mailto:zac@zhd.dev) diff --git a/docs/authors.md b/docs/authors.md new file mode 100644 index 00000000000..a5349b4b9df --- /dev/null +++ b/docs/authors.md @@ -0,0 +1,184 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# Authors + +Glued together by [Łukasz Langa](mailto:lukasz@langa.pl). + +Maintained with [Carol Willing](mailto:carolcode@willingconsulting.com), +[Carl Meyer](mailto:carl@oddbird.net), +[Jelle Zijlstra](mailto:jelle.zijlstra@gmail.com), +[Mika Naylor](mailto:mail@autophagy.io), +[Zsolt Dollenstein](mailto:zsol.zsol@gmail.com), and +[Cooper Lees](mailto:me@cooperlees.com). + +Multiple contributions by: + +- [Abdur-Rahmaan Janhangeer](mailto:arj.python@gmail.com) +- [Adam Johnson](mailto:me@adamj.eu) +- [Adam Williamson](mailto:adamw@happyassassin.net) +- [Alexander Huynh](mailto:github@grande.coffee) +- [Alex Vandiver](mailto:github@chmrr.net) +- [Allan Simon](mailto:allan.simon@supinfo.com) +- Anders-Petter Ljungquist +- [Andrew Thorp](mailto:andrew.thorp.dev@gmail.com) +- [Andrew Zhou](mailto:andrewfzhou@gmail.com) +- [Andrey](mailto:dyuuus@yandex.ru) +- [Andy Freeland](mailto:andy@andyfreeland.net) +- [Anthony Sottile](mailto:asottile@umich.edu) +- [Arjaan Buijk](mailto:arjaan.buijk@gmail.com) +- [Arnav Borbornah](mailto:arnavborborah11@gmail.com) +- [Artem Malyshev](mailto:proofit404@gmail.com) +- [Asger Hautop Drewsen](mailto:asgerdrewsen@gmail.com) +- [Augie Fackler](mailto:raf@durin42.com) +- [Aviskar KC](mailto:aviskarkc10@gmail.com) +- Batuhan Taşkaya +- [Benjamin Wohlwend](mailto:bw@piquadrat.ch) +- [Benjamin Woodruff](mailto:github@benjam.info) +- [Bharat Raghunathan](mailto:bharatraghunthan9767@gmail.com) +- [Brandt Bucher](mailto:brandtbucher@gmail.com) +- [Brett Cannon](mailto:brett@python.org) +- [Bryan Bugyi](mailto:bryan.bugyi@rutgers.edu) +- [Bryan Forbes](mailto:bryan@reigndropsfall.net) +- [Calum Lind](mailto:calumlind@gmail.com) +- [Charles](mailto:peacech@gmail.com) +- Charles Reid +- [Christian Clauss](mailto:cclauss@bluewin.ch) +- [Christian Heimes](mailto:christian@python.org) +- [Chuck Wooters](mailto:chuck.wooters@microsoft.com) +- [Chris Rose](mailto:offline@offby1.net) +- Codey Oxley +- [Cong](mailto:congusbongus@gmail.com) +- [Cooper Ry Lees](mailto:me@cooperlees.com) +- [Dan Davison](mailto:dandavison7@gmail.com) +- [Daniel Hahler](mailto:github@thequod.de) +- [Daniel M. Capella](mailto:polycitizen@gmail.com) +- Daniele Esposti +- [David Hotham](mailto:david.hotham@metaswitch.com) +- [David Lukes](mailto:dafydd.lukes@gmail.com) +- [David Szotten](mailto:davidszotten@gmail.com) +- [Denis Laxalde](mailto:denis@laxalde.org) +- [Douglas Thor](mailto:dthor@transphormusa.com) +- dylanjblack +- [Eli Treuherz](mailto:eli@treuherz.com) +- [Emil Hessman](mailto:emil@hessman.se) +- [Felix Kohlgrüber](mailto:felix.kohlgrueber@gmail.com) +- [Florent Thiery](mailto:fthiery@gmail.com) +- Francisco +- [Giacomo Tagliabue](mailto:giacomo.tag@gmail.com) +- [Greg Gandenberger](mailto:ggandenberger@shoprunner.com) +- [Gregory P. Smith](mailto:greg@krypto.org) +- Gustavo Camargo +- hauntsaninja +- [Heaford](mailto:dan@heaford.com) +- [Hugo Barrera](mailto::hugo@barrera.io) +- Hugo van Kemenade +- [Hynek Schlawack](mailto:hs@ox.cx) +- [Ivan Katanić](mailto:ivan.katanic@gmail.com) +- [Jakub Kadlubiec](mailto:jakub.kadlubiec@skyscanner.net) +- [Jakub Warczarek](mailto:jakub.warczarek@gmail.com) +- [Jan Hnátek](mailto:jan.hnatek@gmail.com) +- [Jason Fried](mailto:me@jasonfried.info) +- [Jason Friedland](mailto:jason@friedland.id.au) +- [jgirardet](mailto:ijkl@netc.fr) +- Jim Brännlund +- [Jimmy Jia](mailto:tesrin@gmail.com) +- [Joe Antonakakis](mailto:jma353@cornell.edu) +- [Jon Dufresne](mailto:jon.dufresne@gmail.com) +- [Jonas Obrist](mailto:ojiidotch@gmail.com) +- [Jonty Wareing](mailto:jonty@jonty.co.uk) +- [Jose Nazario](mailto:jose.monkey.org@gmail.com) +- [Joseph Larson](mailto:larson.joseph@gmail.com) +- [Josh Bode](mailto:joshbode@fastmail.com) +- [Josh Holland](mailto:anowlcalledjosh@gmail.com) +- [José Padilla](mailto:jpadilla@webapplicate.com) +- [Juan Luis Cano Rodríguez](mailto:hello@juanlu.space) +- [kaiix](mailto:kvn.hou@gmail.com) +- [Katie McLaughlin](mailto:katie@glasnt.com) +- Katrin Leinweber +- [Keith Smiley](mailto:keithbsmiley@gmail.com) +- [Kenyon Ralph](mailto:kenyon@kenyonralph.com) +- [Kevin Kirsche](mailto:Kev.Kirsche+GitHub@gmail.com) +- [Kyle Hausmann](mailto:kyle.hausmann@gmail.com) +- [Kyle Sunden](mailto:sunden@wisc.edu) +- Lawrence Chan +- [Linus Groh](mailto:mail@linusgroh.de) +- [Loren Carvalho](mailto:comradeloren@gmail.com) +- [Luka Sterbic](mailto:luka.sterbic@gmail.com) +- [LukasDrude](mailto:mail@lukas-drude.de) +- Mahmoud Hossam +- Mariatta +- [Matt VanEseltine](mailto:vaneseltine@gmail.com) +- [Matthew Clapp](mailto:itsayellow+dev@gmail.com) +- [Matthew Walster](mailto:matthew@walster.org) +- Max Smolens +- [Michael Aquilina](mailto:michaelaquilina@gmail.com) +- [Michael Flaxman](mailto:michael.flaxman@gmail.com) +- [Michael J. Sullivan](mailto:sully@msully.net) +- [Michael McClimon](mailto:michael@mcclimon.org) +- [Miguel Gaiowski](mailto:miggaiowski@gmail.com) +- [Mike](mailto:roshi@fedoraproject.org) +- [mikehoyio](mailto:mikehoy@gmail.com) +- [Min ho Kim](mailto:minho42@gmail.com) +- [Miroslav Shubernetskiy](mailto:miroslav@miki725.com) +- MomIsBestFriend +- [Nathan Goldbaum](mailto:ngoldbau@illinois.edu) +- [Nathan Hunt](mailto:neighthan.hunt@gmail.com) +- [Neraste](mailto:neraste.herr10@gmail.com) +- [Nikolaus Waxweiler](mailto:madigens@gmail.com) +- [Ofek Lev](mailto:ofekmeister@gmail.com) +- [Osaetin Daniel](mailto:osaetindaniel@gmail.com) +- [otstrel](mailto:otstrel@gmail.com) +- [Pablo Galindo](mailto:Pablogsal@gmail.com) +- [Paul Ganssle](mailto:p.ganssle@gmail.com) +- [Paul Meinhardt](mailto:mnhrdt@gmail.com) +- [Peter Bengtsson](mailto:mail@peterbe.com) +- [Peter Stensmyr](mailto:peter.stensmyr@gmail.com) +- pmacosta +- [Quentin Pradet](mailto:quentin@pradet.me) +- [Ralf Schmitt](mailto:ralf@systemexit.de) +- [Ramón Valles](mailto:mroutis@protonmail.com) +- [Richard Fearn](mailto:richardfearn@gmail.com) +- Richard Si +- [Rishikesh Jha](mailto:rishijha424@gmail.com) +- [Rupert Bedford](mailto:rupert@rupertb.com) +- Russell Davis +- [Rémi Verschelde](mailto:rverschelde@gmail.com) +- [Sami Salonen](mailto:sakki@iki.fi) +- [Samuel Cormier-Iijima](mailto:samuel@cormier-iijima.com) +- [Sanket Dasgupta](mailto:sanketdasgupta@gmail.com) +- Sergi +- [Scott Stevenson](mailto:scott@stevenson.io) +- Shantanu +- [shaoran](mailto:shaoran@sakuranohana.org) +- [Shinya Fujino](mailto:shf0811@gmail.com) +- springstan +- [Stavros Korokithakis](mailto:hi@stavros.io) +- [Stephen Rosen](mailto:sirosen@globus.org) +- [Steven M. Vascellaro](mailto:S.Vascellaro@gmail.com) +- [Sunil Kapil](mailto:snlkapil@gmail.com) +- [Sébastien Eustace](mailto:sebastien.eustace@gmail.com) +- [Tal Amuyal](mailto:TalAmuyal@gmail.com) +- [Terrance](mailto:git@terrance.allofti.me) +- [Thom Lu](mailto:thomas.c.lu@gmail.com) +- [Thomas Grainger](mailto:tagrain@gmail.com) +- [Tim Gates](mailto:tim.gates@iress.com) +- [Tim Swast](mailto:swast@google.com) +- [Timo](mailto:timo_tk@hotmail.com) +- Toby Fleming +- [Tom Christie](mailto:tom@tomchristie.com) +- [Tony Narlock](mailto:tony@git-pull.com) +- [Tsuyoshi Hombashi](mailto:tsuyoshi.hombashi@gmail.com) +- [Tushar Chandra](mailto:tusharchandra2018@u.northwestern.edu) +- [Tzu-ping Chung](mailto:uranusjr@gmail.com) +- [Utsav Shah](mailto:ukshah2@illinois.edu) +- utsav-dbx +- vezeli +- [Ville Skyttä](mailto:ville.skytta@iki.fi) +- [Vishwas B Sharma](mailto:sharma.vishwas88@gmail.com) +- [Vlad Emelianov](mailto:volshebnyi@gmail.com) +- [williamfzc](mailto:178894043@qq.com) +- [wouter bolsterlee](mailto:wouter@bolsterl.ee) +- Yazdan +- [Yngve Høiseth](mailto:yngve@hoiseth.net) +- [Yurii Karabas](mailto:1998uriyyo@gmail.com) +- [Zac Hatfield-Dodds](mailto:zac@zhd.dev) diff --git a/docs/black_primer.md b/docs/black_primer.md index af4184233e2..a2dd964b7dc 100644 --- a/docs/black_primer.md +++ b/docs/black_primer.md @@ -71,7 +71,7 @@ each parameter is explained below: "expect_formatting_changes": true, "git_clone_url": "https://github.com/cooperlees/aioexabgp.git", "long_checkout": false, - "py_versions": ["all", "3.8"] // "all" ignores all other versions + "py_versions": ["all", "3.8"] } } } @@ -103,9 +103,9 @@ Failed projects: +++ tests/b303_b304.py 2020-05-17 20:06:42.753851 +0000 @@ -26,11 +26,11 @@ maxint = 5 # this is okay - # the following shouldn't crash + # the following should not crash (a, b, c) = list(range(3)) - # it's different than this + # it is different than this a, b, c = list(range(3)) - a, b, c, = list(range(3)) + a, b, c = list(range(3)) diff --git a/docs/change_log.md b/docs/change_log.md new file mode 100644 index 00000000000..658414bf967 --- /dev/null +++ b/docs/change_log.md @@ -0,0 +1,503 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM CHANGES.md" + +## Change Log + +### 20.8b1 + +#### _Packaging_ + +- explicitly depend on Click 7.1.2 or newer as `Black` no longer works with versions + older than 7.0 + +### 20.8b0 + +#### _Black_ + +- re-implemented support for explicit trailing commas: now it works consistently within + any bracket pair, including nested structures (#1288 and duplicates) + +- `Black` now reindents docstrings when reindenting code around it (#1053) + +- `Black` now shows colored diffs (#1266) + +- `Black` is now packaged using 'py3' tagged wheels (#1388) + +- `Black` now supports Python 3.8 code, e.g. star expressions in return statements + (#1121) + +- `Black` no longer normalizes capital R-string prefixes as those have a + community-accepted meaning (#1244) + +- `Black` now uses exit code 2 when specified configuration file doesn't exit (#1361) + +- `Black` now works on AWS Lambda (#1141) + +- added `--force-exclude` argument (#1032) + +- removed deprecated `--py36` option (#1236) + +- fixed `--diff` output when EOF is encountered (#526) + +- fixed `# fmt: off` handling around decorators (#560) + +- fixed unstable formatting with some `# type: ignore` comments (#1113) + +- fixed invalid removal on organizing brackets followed by indexing (#1575) + +- introduced `black-primer`, a CI tool that allows us to run regression tests against + existing open source users of Black (#1402) + +- introduced property-based fuzzing to our test suite based on Hypothesis and + Hypothersmith (#1566) + +- implemented experimental and disabled by default long string rewrapping (#1132), + hidden under a `--experimental-string-processing` flag while it's being worked on; + this is an undocumented and unsupported feature, you lose Internet points for + depending on it (#1609) + +#### Vim plugin + +- prefer virtualenv packages over global packages (#1383) + +### 19.10b0 + +- added support for PEP 572 assignment expressions (#711) + +- added support for PEP 570 positional-only arguments (#943) + +- added support for async generators (#593) + +- added support for pre-splitting collections by putting an explicit trailing comma + inside (#826) + +- added `black -c` as a way to format code passed from the command line (#761) + +- --safe now works with Python 2 code (#840) + +- fixed grammar selection for Python 2-specific code (#765) + +- fixed feature detection for trailing commas in function definitions and call sites + (#763) + +- `# fmt: off`/`# fmt: on` comment pairs placed multiple times within the same block of + code now behave correctly (#1005) + +- _Black_ no longer crashes on Windows machines with more than 61 cores (#838) + +- _Black_ no longer crashes on standalone comments prepended with a backslash (#767) + +- _Black_ no longer crashes on `from` ... `import` blocks with comments (#829) + +- _Black_ no longer crashes on Python 3.7 on some platform configurations (#494) + +- _Black_ no longer fails on comments in from-imports (#671) + +- _Black_ no longer fails when the file starts with a backslash (#922) + +- _Black_ no longer merges regular comments with type comments (#1027) + +- _Black_ no longer splits long lines that contain type comments (#997) + +- removed unnecessary parentheses around `yield` expressions (#834) + +- added parentheses around long tuples in unpacking assignments (#832) + +- added parentheses around complex powers when they are prefixed by a unary operator + (#646) + +- fixed bug that led _Black_ format some code with a line length target of 1 (#762) + +- _Black_ no longer introduces quotes in f-string subexpressions on string boundaries + (#863) + +- if _Black_ puts parenthesis around a single expression, it moves comments to the + wrapped expression instead of after the brackets (#872) + +- `blackd` now returns the version of _Black_ in the response headers (#1013) + +- `blackd` can now output the diff of formats on source code when the `X-Diff` header is + provided (#969) + +### 19.3b0 + +- new option `--target-version` to control which Python versions _Black_-formatted code + should target (#618) + +- deprecated `--py36` (use `--target-version=py36` instead) (#724) + +- _Black_ no longer normalizes numeric literals to include `_` separators (#696) + +- long `del` statements are now split into multiple lines (#698) + +- type comments are no longer mangled in function signatures + +- improved performance of formatting deeply nested data structures (#509) + +- _Black_ now properly formats multiple files in parallel on Windows (#632) + +- _Black_ now creates cache files atomically which allows it to be used in parallel + pipelines (like `xargs -P8`) (#673) + +- _Black_ now correctly indents comments in files that were previously formatted with + tabs (#262) + +- `blackd` now supports CORS (#622) + +### 18.9b0 + +- numeric literals are now formatted by _Black_ (#452, #461, #464, #469): + + - numeric literals are normalized to include `_` separators on Python 3.6+ code + + - added `--skip-numeric-underscore-normalization` to disable the above behavior and + leave numeric underscores as they were in the input + + - code with `_` in numeric literals is recognized as Python 3.6+ + + - most letters in numeric literals are lowercased (e.g., in `1e10`, `0x01`) + + - hexadecimal digits are always uppercased (e.g. `0xBADC0DE`) + +- added `blackd`, see [its documentation](#blackd) for more info (#349) + +- adjacent string literals are now correctly split into multiple lines (#463) + +- trailing comma is now added to single imports that don't fit on a line (#250) + +- cache is now populated when `--check` is successful for a file which speeds up + consecutive checks of properly formatted unmodified files (#448) + +- whitespace at the beginning of the file is now removed (#399) + +- fixed mangling [pweave](http://mpastell.com/pweave/) and + [Spyder IDE](https://www.spyder-ide.org/) special comments (#532) + +- fixed unstable formatting when unpacking big tuples (#267) + +- fixed parsing of `__future__` imports with renames (#389) + +- fixed scope of `# fmt: off` when directly preceding `yield` and other nodes (#385) + +- fixed formatting of lambda expressions with default arguments (#468) + +- fixed `async for` statements: _Black_ no longer breaks them into separate lines (#372) + +- note: the Vim plugin stopped registering `,=` as a default chord as it turned out to + be a bad idea (#415) + +### 18.6b4 + +- hotfix: don't freeze when multiple comments directly precede `# fmt: off` (#371) + +### 18.6b3 + +- typing stub files (`.pyi`) now have blank lines added after constants (#340) + +- `# fmt: off` and `# fmt: on` are now much more dependable: + + - they now work also within bracket pairs (#329) + + - they now correctly work across function/class boundaries (#335) + + - they now work when an indentation block starts with empty lines or misaligned + comments (#334) + +- made Click not fail on invalid environments; note that Click is right but the + likelihood we'll need to access non-ASCII file paths when dealing with Python source + code is low (#277) + +- fixed improper formatting of f-strings with quotes inside interpolated expressions + (#322) + +- fixed unnecessary slowdown when long list literals where found in a file + +- fixed unnecessary slowdown on AST nodes with very many siblings + +- fixed cannibalizing backslashes during string normalization + +- fixed a crash due to symbolic links pointing outside of the project directory (#338) + +### 18.6b2 + +- added `--config` (#65) + +- added `-h` equivalent to `--help` (#316) + +- fixed improper unmodified file caching when `-S` was used + +- fixed extra space in string unpacking (#305) + +- fixed formatting of empty triple quoted strings (#313) + +- fixed unnecessary slowdown in comment placement calculation on lines without comments + +### 18.6b1 + +- hotfix: don't output human-facing information on stdout (#299) + +- hotfix: don't output cake emoji on non-zero return code (#300) + +### 18.6b0 + +- added `--include` and `--exclude` (#270) + +- added `--skip-string-normalization` (#118) + +- added `--verbose` (#283) + +- the header output in `--diff` now actually conforms to the unified diff spec + +- fixed long trivial assignments being wrapped in unnecessary parentheses (#273) + +- fixed unnecessary parentheses when a line contained multiline strings (#232) + +- fixed stdin handling not working correctly if an old version of Click was used (#276) + +- _Black_ now preserves line endings when formatting a file in place (#258) + +### 18.5b1 + +- added `--pyi` (#249) + +- added `--py36` (#249) + +- Python grammar pickle caches are stored with the formatting caches, making _Black_ + work in environments where site-packages is not user-writable (#192) + +- _Black_ now enforces a PEP 257 empty line after a class-level docstring (and/or + fields) and the first method + +- fixed invalid code produced when standalone comments were present in a trailer that + was omitted from line splitting on a large expression (#237) + +- fixed optional parentheses being removed within `# fmt: off` sections (#224) + +- fixed invalid code produced when stars in very long imports were incorrectly wrapped + in optional parentheses (#234) + +- fixed unstable formatting when inline comments were moved around in a trailer that was + omitted from line splitting on a large expression (#238) + +- fixed extra empty line between a class declaration and the first method if no class + docstring or fields are present (#219) + +- fixed extra empty line between a function signature and an inner function or inner + class (#196) + +### 18.5b0 + +- call chains are now formatted according to the + [fluent interfaces](https://en.wikipedia.org/wiki/Fluent_interface) style (#67) + +- data structure literals (tuples, lists, dictionaries, and sets) are now also always + exploded like imports when they don't fit in a single line (#152) + +- slices are now formatted according to PEP 8 (#178) + +- parentheses are now also managed automatically on the right-hand side of assignments + and return statements (#140) + +- math operators now use their respective priorities for delimiting multiline + expressions (#148) + +- optional parentheses are now omitted on expressions that start or end with a bracket + and only contain a single operator (#177) + +- empty parentheses in a class definition are now removed (#145, #180) + +- string prefixes are now standardized to lowercase and `u` is removed on Python 3.6+ + only code and Python 2.7+ code with the `unicode_literals` future import (#188, #198, + #199) + +- typing stub files (`.pyi`) are now formatted in a style that is consistent with PEP + 484 (#207, #210) + +- progress when reformatting many files is now reported incrementally + +- fixed trailers (content with brackets) being unnecessarily exploded into their own + lines after a dedented closing bracket (#119) + +- fixed an invalid trailing comma sometimes left in imports (#185) + +- fixed non-deterministic formatting when multiple pairs of removable parentheses were + used (#183) + +- fixed multiline strings being unnecessarily wrapped in optional parentheses in long + assignments (#215) + +- fixed not splitting long from-imports with only a single name + +- fixed Python 3.6+ file discovery by also looking at function calls with unpacking. + This fixed non-deterministic formatting if trailing commas where used both in function + signatures with stars and function calls with stars but the former would be + reformatted to a single line. + +- fixed crash on dealing with optional parentheses (#193) + +- fixed "is", "is not", "in", and "not in" not considered operators for splitting + purposes + +- fixed crash when dead symlinks where encountered + +### 18.4a4 + +- don't populate the cache on `--check` (#175) + +### 18.4a3 + +- added a "cache"; files already reformatted that haven't changed on disk won't be + reformatted again (#109) + +- `--check` and `--diff` are no longer mutually exclusive (#149) + +- generalized star expression handling, including double stars; this fixes + multiplication making expressions "unsafe" for trailing commas (#132) + +- _Black_ no longer enforces putting empty lines behind control flow statements (#90) + +- _Black_ now splits imports like "Mode 3 + trailing comma" of isort (#127) + +- fixed comment indentation when a standalone comment closes a block (#16, #32) + +- fixed standalone comments receiving extra empty lines if immediately preceding a + class, def, or decorator (#56, #154) + +- fixed `--diff` not showing entire path (#130) + +- fixed parsing of complex expressions after star and double stars in function calls + (#2) + +- fixed invalid splitting on comma in lambda arguments (#133) + +- fixed missing splits of ternary expressions (#141) + +### 18.4a2 + +- fixed parsing of unaligned standalone comments (#99, #112) + +- fixed placement of dictionary unpacking inside dictionary literals (#111) + +- Vim plugin now works on Windows, too + +- fixed unstable formatting when encountering unnecessarily escaped quotes in a string + (#120) + +### 18.4a1 + +- added `--quiet` (#78) + +- added automatic parentheses management (#4) + +- added [pre-commit](https://pre-commit.com) integration (#103, #104) + +- fixed reporting on `--check` with multiple files (#101, #102) + +- fixed removing backslash escapes from raw strings (#100, #105) + +### 18.4a0 + +- added `--diff` (#87) + +- add line breaks before all delimiters, except in cases like commas, to better comply + with PEP 8 (#73) + +- standardize string literals to use double quotes (almost) everywhere (#75) + +- fixed handling of standalone comments within nested bracketed expressions; _Black_ + will no longer produce super long lines or put all standalone comments at the end of + the expression (#22) + +- fixed 18.3a4 regression: don't crash and burn on empty lines with trailing whitespace + (#80) + +- fixed 18.3a4 regression: `# yapf: disable` usage as trailing comment would cause + _Black_ to not emit the rest of the file (#95) + +- when CTRL+C is pressed while formatting many files, _Black_ no longer freaks out with + a flurry of asyncio-related exceptions + +- only allow up to two empty lines on module level and only single empty lines within + functions (#74) + +### 18.3a4 + +- `# fmt: off` and `# fmt: on` are implemented (#5) + +- automatic detection of deprecated Python 2 forms of print statements and exec + statements in the formatted file (#49) + +- use proper spaces for complex expressions in default values of typed function + arguments (#60) + +- only return exit code 1 when --check is used (#50) + +- don't remove single trailing commas from square bracket indexing (#59) + +- don't omit whitespace if the previous factor leaf wasn't a math operator (#55) + +- omit extra space in kwarg unpacking if it's the first argument (#46) + +- omit extra space in + [Sphinx auto-attribute comments](http://www.sphinx-doc.org/en/stable/ext/autodoc.html#directive-autoattribute) + (#68) + +### 18.3a3 + +- don't remove single empty lines outside of bracketed expressions (#19) + +- added ability to pipe formatting from stdin to stdin (#25) + +- restored ability to format code with legacy usage of `async` as a name (#20, #42) + +- even better handling of numpy-style array indexing (#33, again) + +### 18.3a2 + +- changed positioning of binary operators to occur at beginning of lines instead of at + the end, following + [a recent change to PEP 8](https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b) + (#21) + +- ignore empty bracket pairs while splitting. This avoids very weirdly looking + formattings (#34, #35) + +- remove a trailing comma if there is a single argument to a call + +- if top level functions were separated by a comment, don't put four empty lines after + the upper function + +- fixed unstable formatting of newlines with imports + +- fixed unintentional folding of post scriptum standalone comments into last statement + if it was a simple statement (#18, #28) + +- fixed missing space in numpy-style array indexing (#33) + +- fixed spurious space after star-based unary expressions (#31) + +### 18.3a1 + +- added `--check` + +- only put trailing commas in function signatures and calls if it's safe to do so. If + the file is Python 3.6+ it's always safe, otherwise only safe if there are no `*args` + or `**kwargs` used in the signature or call. (#8) + +- fixed invalid spacing of dots in relative imports (#6, #13) + +- fixed invalid splitting after comma on unpacked variables in for-loops (#23) + +- fixed spurious space in parenthesized set expressions (#7) + +- fixed spurious space after opening parentheses and in default arguments (#14, #17) + +- fixed spurious space after unary operators when the operand was a complex expression + (#15) + +### 18.3a0 + +- first published version, Happy 🍰 Day 2018! + +- alpha quality + +- date-versioned (see: https://calver.org/) diff --git a/docs/compatible_configs.md b/docs/compatible_configs.md index aa7cd96f165..25e959e3281 100644 --- a/docs/compatible_configs.md +++ b/docs/compatible_configs.md @@ -23,6 +23,7 @@ multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True +ensure_newline_before_comments = True line_length = 88 ``` @@ -62,7 +63,15 @@ The option `force_grid_wrap = 0` is just to tell isort to only wrap imports that the `line_length` limit. Finally, isort should be told to wrap imports when they surpass _Black_'s default limit -of 88 characters via `line_length = 88`. +of 88 characters via `line_length = 88` as well as +`ensure_newline_before_comments = True` to ensure spacing import sections with comments +works the same as with _Black_. + +**Please note** `ensure_newline_before_comments = True` only works since isort >= 5 but +does not break older versions so you can keep it if you are running previous versions. +If only isort >= 5 is used you can add `profile = black` instead of all the options +since [profiles](https://timothycrosley.github.io/isort/docs/configuration/profiles/) +are available and do the configuring for you. ### Formats @@ -75,6 +84,7 @@ multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True +ensure_newline_before_comments = True line_length = 88 ``` @@ -89,6 +99,7 @@ multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True +ensure_newline_before_comments = True line_length = 88 ``` @@ -103,6 +114,7 @@ multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true +ensure_newline_before_comments = true line_length = 88 ``` @@ -117,6 +129,7 @@ multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True +ensure_newline_before_comments = True line_length = 88 ``` @@ -228,7 +241,7 @@ characters via `max-line-length = 88`.
pylintrc -```rc +```ini [MESSAGES CONTROL] disable = C0330, C0326 diff --git a/docs/conf.py b/docs/conf.py index 3343087cfbd..7381c9d6423 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,20 +15,17 @@ from pathlib import Path import re import string -from typing import Callable, List, Optional, Pattern, Tuple, Set +from typing import Callable, Dict, List, Optional, Pattern, Tuple, Set from dataclasses import dataclass -import os import logging from pkg_resources import get_distribution -from recommonmark.parser import CommonMarkParser logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO) LOG = logging.getLogger(__name__) -# Get a relative path so logs printing out SRC isn't too long. -CURRENT_DIR = Path(__file__).parent.relative_to(os.getcwd()) +CURRENT_DIR = Path(__file__).parent README = CURRENT_DIR / ".." / "README.md" REFERENCE_DIR = CURRENT_DIR / "reference" STATIC_DIR = CURRENT_DIR / "_static" @@ -102,7 +99,13 @@ def get_contents(section: DocSection) -> str: for lineno, line in enumerate(f, start=1): if lineno >= start_line and lineno < end_line: contents.append(line) - return "".join(contents) + result = "".join(contents) + # Let's make Prettier happy with the amount of trailing newlines in the sections. + if result.endswith("\n\n"): + result = result[:-1] + if not result.endswith("\n"): + result = result + "\n" + return result def get_sections_from_readme() -> List[DocSection]: @@ -162,18 +165,19 @@ def process_sections( It processes custom sections before the README generated sections so sections in the README can be overwritten with custom options. """ - processed_sections: Set[str] = set() + processed_sections: Dict[str, DocSection] = {} modified_files: Set[Path] = set() sections: List[DocSection] = custom_sections sections.extend(readme_sections) for section in sections: - LOG.info(f"Processing '{section.name}' from {section.src}") if section.name in processed_sections: - LOG.info( + LOG.warning( f"Skipping '{section.name}' from '{section.src}' as it is a duplicate" + f" of a custom section from '{processed_sections[section.name].src}'" ) continue + LOG.info(f"Processing '{section.name}' from '{section.src}'") target_path: Path = CURRENT_DIR / section.get_out_filename() if target_path in modified_files: LOG.warning( @@ -187,20 +191,18 @@ def process_sections( contents = fix_headers(contents) with open(target_path, "w", encoding="utf-8") as f: - if section.src.suffix == ".md": - f.write( - "[//]: # (NOTE: THIS FILE WAS AUTOGENERATED FROM" - f" {section.src})\n\n" - ) + if section.src.suffix == ".md" and section.src != target_path: + rel = section.src.resolve().relative_to(CURRENT_DIR.parent) + f.write(f'[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM {rel}"\n\n') f.write(contents) - processed_sections.add(section.name) + processed_sections[section.name] = section modified_files.add(target_path) # -- Project information ----------------------------------------------------- project = "Black" -copyright = "2018, Łukasz Langa and contributors to Black" +copyright = "2020, Łukasz Langa and contributors to Black" author = "Łukasz Langa and contributors to Black" # Autopopulate version @@ -212,8 +214,7 @@ def process_sections( version = version.split(sp)[0] custom_sections = [ - DocSection("the_black_code_style", CURRENT_DIR / "the_black_code_style.md",), - DocSection("pragmatism", CURRENT_DIR / "the_black_code_style.md",), + DocSection("the_black_code_style", CURRENT_DIR / "the_black_code_style.md"), DocSection("editor_integration", CURRENT_DIR / "editor_integration.md"), DocSection("blackd", CURRENT_DIR / "blackd.md"), DocSection("black_primer", CURRENT_DIR / "black_primer.md"), @@ -221,28 +222,47 @@ def process_sections( DocSection("change_log", CURRENT_DIR / ".." / "CHANGES.md"), ] +# Sphinx complains when there is a source file that isn't referenced in any of the docs. +# Since some sections autogenerated from the README are unused warnings will appear. +# +# Sections must be listed to what their name is when passed through make_filename(). +blocklisted_sections_from_readme = { + "license", + "pragmatism", + "testimonials", + "used_by", +} make_pypi_svg(release) readme_sections = get_sections_from_readme() +readme_sections = [ + x for x in readme_sections if x.name not in blocklisted_sections_from_readme +] + process_sections(custom_sections, readme_sections) # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' +needs_sphinx = "3.0" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.napoleon"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "recommonmark", +] + +# If you need extensions of a certain version or higher, list them here. +needs_extensions = {"recommonmark": "0.5"} # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] -source_parsers = {".md": CommonMarkParser} - # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: source_suffix = [".rst", ".md"] diff --git a/docs/contributing_to_black.md b/docs/contributing_to_black.md new file mode 100644 index 00000000000..e5307adb5d0 --- /dev/null +++ b/docs/contributing_to_black.md @@ -0,0 +1,70 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM CONTRIBUTING.md" + +# Contributing to _Black_ + +Welcome! Happy to see you willing to make the project better. Have you read the entire +[user documentation](https://black.readthedocs.io/en/latest/) yet? + +## Bird's eye view + +In terms of inspiration, _Black_ is about as configurable as _gofmt_. This is +deliberate. + +Bug reports and fixes are always welcome! Please follow the +[issue template on GitHub](https://github.com/psf/black/issues/new) for best results. + +Before you suggest a new feature or configuration knob, ask yourself why you want it. If +it enables better integration with some workflow, fixes an inconsistency, speeds things +up, and so on - go for it! On the other hand, if your answer is "because I don't like a +particular formatting" then you're not ready to embrace _Black_ yet. Such changes are +unlikely to get accepted. You can still try but prepare to be disappointed. + +## Technicalities + +Development on the latest version of Python is preferred. As of this writing it's 3.8. +You can use any operating system. I am using macOS myself and CentOS at work. + +Install all development dependencies using: + +```console +$ pipenv install --dev +$ pipenv shell +$ pre-commit install +``` + +If you haven't used `pipenv` before but are comfortable with virtualenvs, just run +`pip install pipenv` in the virtualenv you're already using and invoke the command above +from the cloned _Black_ repo. It will do the correct thing. + +Before submitting pull requests, run lints and tests with: + +```console +$ pre-commit run -a +$ python -m unittest +$ black-primer [-k -w /tmp/black_test_repos] +``` + +## black-primer + +`black-primer` is used by CI to pull down well-known _Black_ formatted projects and see +if we get source code changes. It will error on formatting changes or errors. Please run +before pushing your PR to see if you get the actions you would expect from _Black_ with +your PR. You may need to change +[primer.json](https://github.com/psf/black/blob/master/src/black_primer/primer.json) +configuration for it to pass. + +For more `black-primer` information visit the +[documentation](https://github.com/psf/black/blob/master/docs/black_primer.md). + +## Hygiene + +If you're fixing a bug, add a test. Run it first to confirm it fails, then fix the bug, +run it again to confirm it's really fixed. + +If adding a new feature, add a test. In fact, always add a test. But wait, before adding +any large feature, first open an issue for us to discuss the idea first. + +## Finally + +Thanks again for your interest in improving the project! You're taking action when most +people decide to sit and watch. diff --git a/docs/editor_integration.md b/docs/editor_integration.md index 00241f23335..73107d6a4a1 100644 --- a/docs/editor_integration.md +++ b/docs/editor_integration.md @@ -146,7 +146,7 @@ or you can copy the plugin from ``` mkdir -p ~/.vim/pack/python/start/black/plugin -curl https://raw.githubusercontent.com/psf/black/master/plugin/black.vim -o ~/.vim/pack/python/start/black/plugin/black.vim +curl https://raw.githubusercontent.com/psf/black/stable/plugin/black.vim -o ~/.vim/pack/python/start/black/plugin/black.vim ``` Let me know if this requires any changes to work with Vim 8's builtin `packadd`, or @@ -255,6 +255,10 @@ Sublime Text, Visual Studio Code and many more), you can use the Use [python-black](https://atom.io/packages/python-black). +## Gradle (the build tool) + +Use the [Spotless](https://github.com/diffplug/spotless/tree/main/plugin-gradle) plugin. + ## Kakoune Add the following hook to your kakrc, then run _Black_ with `:format`. @@ -269,9 +273,9 @@ hook global WinSetOption filetype=python %{ Use [Thonny-black-code-format](https://github.com/Franccisco/thonny-black-code-format). -## Other editors +## Other integrations -Other editors will require external contributions. +Other editors and tools will require external contributions. Patches welcome! ✨ 🍰 ✨ diff --git a/docs/github_actions.md b/docs/github_actions.md new file mode 100644 index 00000000000..7ff87540242 --- /dev/null +++ b/docs/github_actions.md @@ -0,0 +1,19 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# GitHub Actions + +Create a file named `.github/workflows/black.yml` inside your repository with: + +```yaml +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: psf/black@stable +``` diff --git a/docs/ignoring_unmodified_files.md b/docs/ignoring_unmodified_files.md new file mode 100644 index 00000000000..a915f4e8678 --- /dev/null +++ b/docs/ignoring_unmodified_files.md @@ -0,0 +1,23 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# Ignoring unmodified files + +_Black_ remembers files it has already formatted, unless the `--diff` flag is used or +code is passed via standard input. This information is stored per-user. The exact +location of the file depends on the _Black_ version and the system on which _Black_ is +run. The file is non-portable. The standard location on common operating systems is: + +- Windows: + `C:\\Users\\AppData\Local\black\black\Cache\\cache...pickle` +- macOS: + `/Users//Library/Caches/black//cache...pickle` +- Linux: + `/home//.cache/black//cache...pickle` + +`file-mode` is an int flag that determines whether the file was formatted as 3.6+ only, +as .pyi, and whether string normalization was omitted. + +To override the location of these files on macOS or Linux, set the environment variable +`XDG_CACHE_HOME` to your preferred location. For example, if you want to put the cache +in the directory you're running _Black_ from, set `XDG_CACHE_HOME=.cache`. _Black_ will +then write the above files to `.cache/black//`. diff --git a/docs/index.rst b/docs/index.rst index 676644ec6c6..f03d247d949 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,10 +51,12 @@ Contents installation_and_usage the_black_code_style pyproject_toml + compatible_configs editor_integration blackd black_primer version_control_integration + github_actions ignoring_unmodified_files contributing_to_black show_your_style @@ -66,5 +68,4 @@ Indices and tables ================== * :ref:`genindex` -* :ref:`modindex` * :ref:`search` diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md new file mode 100644 index 00000000000..cc0269198a2 --- /dev/null +++ b/docs/installation_and_usage.md @@ -0,0 +1,179 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# Installation and usage + +## Installation + +_Black_ can be installed by running `pip install black`. It requires Python 3.6.0+ to +run but you can reformat Python 2 code with it, too. + +### Install from GitHub + +If you can't wait for the latest _hotness_ and want to install from GitHub, use: + +`pip install git+git://github.com/psf/black` + +## Usage + +To get started right away with sensible defaults: + +```sh +black {source_file_or_directory} +``` + +You can run _Black_ as a package if running it as a script doesn't work: + +```sh +python -m black {source_file_or_directory} +``` + +## Command line options + +_Black_ doesn't provide many options. You can list them by running `black --help`: + +```text +Usage: black [OPTIONS] [SRC]... + + The uncompromising code formatter. + +Options: + -c, --code TEXT Format the code passed in as a string. + -l, --line-length INTEGER How many characters per line to allow. + [default: 88] + + -t, --target-version [py27|py33|py34|py35|py36|py37|py38] + Python versions that should be supported by + Black's output. [default: per-file auto- + detection] + + --pyi Format all input files like typing stubs + regardless of file extension (useful when + piping source on standard input). + + -S, --skip-string-normalization + Don't normalize string quotes or prefixes. + --check Don't write the files back, just return the + status. Return code 0 means nothing would + change. Return code 1 means some files + would be reformatted. Return code 123 means + there was an internal error. + + --diff Don't write the files back, just output a + diff for each file on stdout. + + --color / --no-color Show colored diff. Only applies when + `--diff` is given. + + --fast / --safe If --fast given, skip temporary sanity + checks. [default: --safe] + + --include TEXT A regular expression that matches files and + directories that should be included on + recursive searches. An empty value means + all files are included regardless of the + name. Use forward slashes for directories + on all platforms (Windows, too). Exclusions + are calculated first, inclusions later. + [default: \.pyi?$] + + --exclude TEXT A regular expression that matches files and + directories that should be excluded on + recursive searches. An empty value means no + paths are excluded. Use forward slashes for + directories on all platforms (Windows, too). + Exclusions are calculated first, inclusions + later. [default: /(\.eggs|\.git|\.hg|\.mypy + _cache|\.nox|\.tox|\.venv|\.svn|_build|buck- + out|build|dist)/] + + --force-exclude TEXT Like --exclude, but files and directories + matching this regex will be excluded even + when they are passed explicitly as arguments + + -q, --quiet Don't emit non-error messages to stderr. + Errors are still emitted; silence those with + 2>/dev/null. + + -v, --verbose Also emit messages to stderr about files + that were not changed or were ignored due to + --exclude=. + + --version Show the version and exit. + --config FILE Read configuration from FILE path. + -h, --help Show this message and exit. +``` + +_Black_ is a well-behaved Unix-style command-line tool: + +- it does nothing if no sources are passed to it; +- it will read from standard input and write to standard output if `-` is used as the + filename; +- it only outputs messages to users on standard error; +- exits with code 0 unless an internal error occurred (or `--check` was used). + +## Using _Black_ with other tools + +While _Black_ enforces formatting that conforms to PEP 8, other tools may raise warnings +about _Black_'s changes or will overwrite _Black_'s changes. A good example of this is +[isort](https://pypi.org/p/isort). Since _Black_ is barely configurable, these tools +should be configured to neither warn about nor overwrite _Black_'s changes. + +Actual details on _Black_ compatible configurations for various tools can be found in +[compatible_configs](https://github.com/psf/black/blob/master/docs/compatible_configs.md). + +## Migrating your code style without ruining git blame + +A long-standing argument against moving to automated code formatters like _Black_ is +that the migration will clutter up the output of `git blame`. This was a valid argument, +but since Git version 2.23, Git natively supports +[ignoring revisions in blame](https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revltrevgt) +with the `--ignore-rev` option. You can also pass a file listing the revisions to ignore +using the `--ignore-revs-file` option. The changes made by the revision will be ignored +when assigning blame. Lines modified by an ignored revision will be blamed on the +previous revision that modified those lines. + +So when migrating your project's code style to _Black_, reformat everything and commit +the changes (preferably in one massive commit). Then put the full 40 characters commit +identifier(s) into a file. + +``` +# Migrate code style to Black +5b4ab991dede475d393e9d69ec388fd6bd949699 +``` + +Afterwards, you can pass that file to `git blame` and see clean and meaningful blame +information. + +```console +$ git blame important.py --ignore-revs-file .git-blame-ignore-revs +7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 1) def very_important_function(text, file): +abdfd8b0 (Alice Doe 2019-09-23 11:39:32 -0400 2) text = text.lstrip() +7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 3) with open(file, "r+") as f: +7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 4) f.write(formatted) +``` + +You can even configure `git` to automatically ignore revisions listed in a file on every +call to `git blame`. + +```console +$ git config blame.ignoreRevsFile .git-blame-ignore-revs +``` + +**The one caveat is that GitHub and GitLab do not yet support ignoring revisions using +their native UI of blame.** So blame information will be cluttered with a reformatting +commit on those platforms. (If you'd like this feature, there's an open issue for +[GitLab](https://gitlab.com/gitlab-org/gitlab/-/issues/31423) and please let GitHub +know!) + +## NOTE: This is a beta product + +_Black_ is already [successfully used](#used-by) by many projects, small and big. It +also sports a decent test suite. However, it is still very new. Things will probably be +wonky for a while. This is made explicit by the "Beta" trove classifier, as well as by +the "b" in the version number. What this means for you is that **until the formatter +becomes stable, you should expect some formatting to change in the future**. That being +said, no drastic stylistic changes are planned, mostly responses to bug reports. + +Also, as a temporary safety measure, _Black_ will check that the reformatted code still +produces a valid AST that is equivalent to the original. This slows it down. If you're +feeling confident, use `--fast`. diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md new file mode 100644 index 00000000000..cd313452b1e --- /dev/null +++ b/docs/pyproject_toml.md @@ -0,0 +1,88 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# pyproject.toml + +_Black_ is able to read project-specific default values for its command line options +from a `pyproject.toml` file. This is especially useful for specifying custom +`--include` and `--exclude` patterns for your project. + +**Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is +"No". _Black_ is all about sensible defaults. + +## What on Earth is a `pyproject.toml` file? + +[PEP 518](https://www.python.org/dev/peps/pep-0518/) defines `pyproject.toml` as a +configuration file to store build system requirements for Python projects. With the help +of tools like [Poetry](https://python-poetry.org/) or +[Flit](https://flit.readthedocs.io/en/latest/) it can fully replace the need for +`setup.py` and `setup.cfg` files. + +## Where _Black_ looks for the file + +By default _Black_ looks for `pyproject.toml` starting from the common base directory of +all files and directories passed on the command line. If it's not there, it looks in +parent directories. It stops looking when it finds the file, or a `.git` directory, or a +`.hg` directory, or the root of the file system, whichever comes first. + +If you're formatting standard input, _Black_ will look for configuration starting from +the current working directory. + +You can also explicitly specify the path to a particular file that you want with +`--config`. In this situation _Black_ will not look for any other file. + +If you're running with `--verbose`, you will see a blue message if a file was found and +used. + +Please note `blackd` will not use `pyproject.toml` configuration. + +## Configuration format + +As the file extension suggests, `pyproject.toml` is a +[TOML](https://github.com/toml-lang/toml) file. It contains separate sections for +different tools. _Black_ is using the `[tool.black]` section. The option keys are the +same as long names of options on the command line. + +Note that you have to use single-quoted strings in TOML for regular expressions. It's +the equivalent of r-strings in Python. Multiline strings are treated as verbose regular +expressions by Black. Use `[ ]` to denote a significant space character. + +
+Example pyproject.toml + +```toml +[tool.black] +line-length = 88 +target-version = ['py37'] +include = '\.pyi?$' +exclude = ''' + +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + )/ + | foo.py # also separately exclude a file named foo.py in + # the root of the project +) +''' +``` + +
+ +## Lookup hierarchy + +Command-line options have defaults that you can see in `--help`. A `pyproject.toml` can +override those defaults. Finally, options provided by the user on the command line +override both. + +_Black_ will only ever use one `pyproject.toml` file during an entire run. It doesn't +look for multiple files, and doesn't compose configuration from different levels of the +file hierarchy. diff --git a/docs/reference/reference_functions.rst b/docs/reference/reference_functions.rst index b10eea9b01f..a7184115c94 100644 --- a/docs/reference/reference_functions.rst +++ b/docs/reference/reference_functions.rst @@ -89,7 +89,7 @@ Split functions .. autofunction:: black.standalone_comment_split -.. autofunction:: black.split_line +.. autofunction:: black.transform_line Caching ------- @@ -171,7 +171,7 @@ Utilities .. autofunction:: black.re_compile_maybe_verbose -.. autofunction:: black.should_explode +.. autofunction:: black.should_split_body_explode .. autofunction:: black.shutdown diff --git a/docs/requirements.txt b/docs/requirements.txt index a36fd8a675b..4cad9bc205b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ -recommonmark==0.4.0 -Sphinx==1.7.2 +recommonmark==0.6.0 +Sphinx==3.2.1 +Pygments==2.6.1 \ No newline at end of file diff --git a/docs/show_your_style.md b/docs/show_your_style.md new file mode 100644 index 00000000000..67b213c3965 --- /dev/null +++ b/docs/show_your_style.md @@ -0,0 +1,19 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# Show your style + +Use the badge in your project's README.md: + +```md +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +``` + +Using the badge in README.rst: + +``` +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black +``` + +Looks like this: +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) diff --git a/docs/the_black_code_style.md b/docs/the_black_code_style.md index 21f217d388e..09d58307a05 100644 --- a/docs/the_black_code_style.md +++ b/docs/the_black_code_style.md @@ -153,13 +153,14 @@ the following configuration.
A compatible `.isort.cfg` -``` +```cfg [settings] -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +ensure_newline_before_comments = True +line_length = 88 ``` The equivalent command line is: diff --git a/docs/version_control_integration.md b/docs/version_control_integration.md new file mode 100644 index 00000000000..25dac308c47 --- /dev/null +++ b/docs/version_control_integration.md @@ -0,0 +1,28 @@ +[//]: # "NOTE: THIS FILE WAS AUTOGENERATED FROM README.md" + +# Version control integration + +Use [pre-commit](https://pre-commit.com/). Once you +[have it installed](https://pre-commit.com/#install), add this to the +`.pre-commit-config.yaml` in your repository: + +```yaml +repos: + - repo: https://github.com/psf/black + rev: 19.10b0 # Replace by any tag/version: https://github.com/psf/black/tags + hooks: + - id: black + language_version: python3 # Should be a command that runs python3.6+ +``` + +Then run `pre-commit install` and you're ready to go. + +Avoid using `args` in the hook. Instead, store necessary configuration in +`pyproject.toml` so that editors and command-line usage of Black all behave consistently +for your project. See _Black_'s own +[pyproject.toml](https://github.com/psf/black/blob/master/pyproject.toml) for an +example. + +If you're already using Python 3.7, switch the `language_version` accordingly. Finally, +`stable` is a branch that tracks the latest release on PyPI. If you'd rather run on +master, this is also an option. diff --git a/fuzz.py b/fuzz.py new file mode 100644 index 00000000000..fdd4917f2ec --- /dev/null +++ b/fuzz.py @@ -0,0 +1,59 @@ +"""Property-based tests for Black. + +By Zac Hatfield-Dodds, based on my Hypothesmith tool for source code +generation. You can run this file with `python`, `pytest`, or (soon) +a coverage-guided fuzzer I'm working on. +""" + +import hypothesmith +from hypothesis import HealthCheck, given, settings, strategies as st + +import black + + +# This test uses the Hypothesis and Hypothesmith libraries to generate random +# syntatically-valid Python source code and run Black in odd modes. +@settings( + max_examples=1000, # roughly 1k tests/minute, or half that under coverage + derandomize=True, # deterministic mode to avoid CI flakiness + deadline=None, # ignore Hypothesis' health checks; we already know that + suppress_health_check=HealthCheck.all(), # this is slow and filter-heavy. +) +@given( + # Note that while Hypothesmith might generate code unlike that written by + # humans, it's a general test that should pass for any *valid* source code. + # (so e.g. running it against code scraped of the internet might also help) + src_contents=hypothesmith.from_grammar() | hypothesmith.from_node(), + # Using randomly-varied modes helps us to exercise less common code paths. + mode=st.builds( + black.FileMode, + line_length=st.just(88) | st.integers(0, 200), + string_normalization=st.booleans(), + is_pyi=st.booleans(), + ), +) +def test_idempotent_any_syntatically_valid_python( + src_contents: str, mode: black.FileMode +) -> None: + # Before starting, let's confirm that the input string is valid Python: + compile(src_contents, "", "exec") # else the bug is in hypothesmith + + # Then format the code... + try: + dst_contents = black.format_str(src_contents, mode=mode) + except black.InvalidInput: + # This is a bug - if it's valid Python code, as above, black should be + # able to code with it. See issues #970, #1012, #1358, and #1557. + # TODO: remove this try-except block when issues are resolved. + return + + # And check that we got equivalent and stable output. + black.assert_equivalent(src_contents, dst_contents) + black.assert_stable(src_contents, dst_contents, mode=mode) + + # Future test: check that pure-python and mypyc versions of black + # give identical output for identical input? + + +if __name__ == "__main__": + test_idempotent_any_syntatically_valid_python() diff --git a/gallery/gallery.py b/gallery/gallery.py index 2a56b4ed4c0..6b42ec3a6d4 100755 --- a/gallery/gallery.py +++ b/gallery/gallery.py @@ -127,7 +127,10 @@ def get_package( def download_and_extract_top_packages( - directory: Path, days: Days = 365, workers: int = 8, limit: slice = DEFAULT_SLICE, + directory: Path, + days: Days = 365, + workers: int = 8, + limit: slice = DEFAULT_SLICE, ) -> Generator[Path, None, None]: with ThreadPoolExecutor(max_workers=workers) as executor: bound_downloader = partial(get_package, version=None, directory=directory) diff --git a/setup.py b/setup.py index bff439c6d8c..12fde2568cf 100644 --- a/setup.py +++ b/setup.py @@ -68,10 +68,9 @@ def get_long_description() -> str: python_requires=">=3.6", zip_safe=False, install_requires=[ - "click>=6.5", - "attrs>=18.1.0", + "click>=7.1.2", "appdirs", - "toml>=0.9.4", + "toml>=0.10.1", "typed-ast>=1.4.0", "regex>=2020.1.8", "pathspec>=0.6, <1", diff --git a/src/black/__init__.py b/src/black/__init__.py index a77a410fa42..1d20bb416ac 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -65,7 +65,7 @@ import colorama # noqa: F401 DEFAULT_LINE_LENGTH = 88 -DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950 +DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950 DEFAULT_INCLUDES = r"\.pyi?$" CACHE_DIR = Path(user_cache_dir("black", version=__version__)) @@ -195,6 +195,7 @@ class Feature(Enum): ASYNC_KEYWORDS = 7 ASSIGNMENT_EXPRESSIONS = 8 POS_ONLY_ARGUMENTS = 9 + FORCE_OPTIONAL_PARENTHESES = 50 VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { @@ -240,6 +241,7 @@ class Mode: target_versions: Set[TargetVersion] = field(default_factory=set) line_length: int = DEFAULT_LINE_LENGTH string_normalization: bool = True + experimental_string_processing: bool = False is_pyi: bool = False def get_cache_key(self) -> str: @@ -376,6 +378,15 @@ def target_version_option_callback( is_flag=True, help="Don't normalize string quotes or prefixes.", ) +@click.option( + "--experimental-string-processing", + is_flag=True, + hidden=True, + help=( + "Experimental option that performs more normalization on string literals." + " Currently disabled because it leads to some crashes." + ), +) @click.option( "--check", is_flag=True, @@ -471,7 +482,7 @@ def target_version_option_callback( ), is_eager=True, callback=read_pyproject_toml, - help="Read configuration from PATH.", + help="Read configuration from FILE path.", ) @click.pass_context def main( @@ -485,6 +496,7 @@ def main( fast: bool, pyi: bool, skip_string_normalization: bool, + experimental_string_processing: bool, quiet: bool, verbose: bool, include: str, @@ -505,6 +517,7 @@ def main( line_length=line_length, is_pyi=pyi, string_normalization=not skip_string_normalization, + experimental_string_processing=experimental_string_processing, ) if config and verbose: out(f"Using configuration from {config}.", bold=False, fg="blue") @@ -583,9 +596,7 @@ def get_sources( root = find_project_root(src) sources: Set[Path] = set() path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx) - exclude_regexes = [exclude_regex] - if force_exclude_regex is not None: - exclude_regexes.append(force_exclude_regex) + gitignore = get_gitignore(root) for s in src: p = Path(s) @@ -595,19 +606,30 @@ def get_sources( p.iterdir(), root, include_regex, - exclude_regexes, + exclude_regex, + force_exclude_regex, report, - get_gitignore(root), + gitignore, ) ) elif s == "-": sources.add(p) elif p.is_file(): - sources.update( - gen_python_files( - [p], root, None, exclude_regexes, report, get_gitignore(root) - ) - ) + normalized_path = normalize_path_maybe_ignore(p, root, report) + if normalized_path is None: + continue + + normalized_path = "/" + normalized_path + # Hard-exclude any files that matches the `--force-exclude` regex. + if force_exclude_regex: + force_exclude_match = force_exclude_regex.search(normalized_path) + else: + force_exclude_match = None + if force_exclude_match and force_exclude_match.group(0): + report.path_ignored(p, "matches the --force-exclude regular expression") + continue + + sources.add(p) else: err(f"invalid path: {s}") return sources @@ -655,6 +677,8 @@ def reformat_one( write_cache(cache, [src], mode) report.done(src, changed) except Exception as exc: + if report.verbose: + traceback.print_exc() report.failed(src, str(exc)) @@ -929,6 +953,7 @@ def f(arg: str = "") -> None: ... A more complex example: + >>> print( ... black.format_str( ... "def f(arg:str='')->None: hey", @@ -973,10 +998,7 @@ def f( before, after = elt.maybe_empty_lines(current_line) dst_contents.append(str(empty_line) * before) for line in transform_line( - current_line, - line_length=mode.line_length, - normalize_strings=mode.string_normalization, - features=split_line_features, + current_line, mode=mode, features=split_line_features ): dst_contents.append(str(line)) return "".join(dst_contents) @@ -1263,6 +1285,7 @@ class BracketTracker: previous: Optional[Leaf] = None _for_loop_depths: List[int] = field(default_factory=list) _lambda_argument_depths: List[int] = field(default_factory=list) + invisible: List[Leaf] = field(default_factory=list) def mark(self, leaf: Leaf) -> None: """Mark `leaf` with bracket-related metadata. Keep track of delimiters. @@ -1288,6 +1311,8 @@ def mark(self, leaf: Leaf) -> None: self.depth -= 1 opening_bracket = self.bracket_match.pop((self.depth, leaf.type)) leaf.opening_bracket = opening_bracket + if not leaf.value: + self.invisible.append(leaf) leaf.bracket_depth = self.depth if self.depth == 0: delim = is_split_before_delimiter(leaf, self.previous) @@ -1300,6 +1325,8 @@ def mark(self, leaf: Leaf) -> None: if leaf.type in OPENING_BRACKETS: self.bracket_match[self.depth, BRACKET[leaf.type]] = leaf self.depth += 1 + if not leaf.value: + self.invisible.append(leaf) self.previous = leaf self.maybe_increment_lambda_arguments(leaf) self.maybe_increment_for_loop_variable(leaf) @@ -1421,7 +1448,8 @@ def append(self, leaf: Leaf, preformatted: bool = False) -> None: ) if self.inside_brackets or not preformatted: self.bracket_tracker.mark(leaf) - self.maybe_remove_trailing_comma(leaf) + if self.maybe_should_explode(leaf): + self.should_explode = True if not self.append_comment(leaf): self.leaves.append(leaf) @@ -1473,69 +1501,6 @@ def is_stub_class(self) -> bool: Leaf(token.DOT, ".") for _ in range(3) ] - @property - def is_collection_with_optional_trailing_comma(self) -> bool: - """Is this line a collection literal with a trailing comma that's optional? - - Note that the trailing comma in a 1-tuple is not optional. - """ - if not self.leaves or len(self.leaves) < 4: - return False - - # Look for and address a trailing colon. - if self.leaves[-1].type == token.COLON: - closer = self.leaves[-2] - close_index = -2 - else: - closer = self.leaves[-1] - close_index = -1 - if closer.type not in CLOSING_BRACKETS or self.inside_brackets: - return False - - if closer.type == token.RPAR: - # Tuples require an extra check, because if there's only - # one element in the tuple removing the comma unmakes the - # tuple. - # - # We also check for parens before looking for the trailing - # comma because in some cases (eg assigning a dict - # literal) the literal gets wrapped in temporary parens - # during parsing. This case is covered by the - # collections.py test data. - opener = closer.opening_bracket - for _open_index, leaf in enumerate(self.leaves): - if leaf is opener: - break - - else: - # Couldn't find the matching opening paren, play it safe. - return False - - commas = 0 - comma_depth = self.leaves[close_index - 1].bracket_depth - for leaf in self.leaves[_open_index + 1 : close_index]: - if leaf.bracket_depth == comma_depth and leaf.type == token.COMMA: - commas += 1 - if commas > 1: - # We haven't looked yet for the trailing comma because - # we might also have caught noop parens. - return self.leaves[close_index - 1].type == token.COMMA - - elif commas == 1: - return False # it's either a one-tuple or didn't have a trailing comma - - if self.leaves[close_index - 1].type in CLOSING_BRACKETS: - close_index -= 1 - closer = self.leaves[close_index] - if closer.type == token.RPAR: - # TODO: this is a gut feeling. Will we ever see this? - return False - - if self.leaves[close_index - 1].type != token.COMMA: - return False - - return True - @property def is_def(self) -> bool: """Is this a function definition? (Also returns True for async defs.)""" @@ -1660,42 +1625,28 @@ def contains_unsplittable_type_ignore(self) -> bool: def contains_multiline_strings(self) -> bool: return any(is_multiline_string(leaf) for leaf in self.leaves) - def maybe_remove_trailing_comma(self, closing: Leaf) -> bool: - """Remove trailing comma if there is one and it's safe.""" - if not (self.leaves and self.leaves[-1].type == token.COMMA): - return False - - # We remove trailing commas only in the case of importing a - # single name from a module. + def maybe_should_explode(self, closing: Leaf) -> bool: + """Return True if this line should explode (always be split), that is when: + - there's a trailing comma here; and + - it's not a one-tuple. + """ if not ( - self.leaves - and self.is_import - and len(self.leaves) > 4 + closing.type in CLOSING_BRACKETS + and self.leaves and self.leaves[-1].type == token.COMMA - and closing.type in CLOSING_BRACKETS - and self.leaves[-4].type == token.NAME - and ( - # regular `from foo import bar,` - self.leaves[-4].value == "import" - # `from foo import (bar as baz,) - or ( - len(self.leaves) > 6 - and self.leaves[-6].value == "import" - and self.leaves[-3].value == "as" - ) - # `from foo import bar as baz,` - or ( - len(self.leaves) > 5 - and self.leaves[-5].value == "import" - and self.leaves[-3].value == "as" - ) - ) - and closing.type == token.RPAR ): return False - self.remove_trailing_comma() - return True + if closing.type in {token.RBRACE, token.RSQB}: + return True + + if self.is_import: + return True + + if not is_one_tuple_between(closing.opening_bracket, closing, self.leaves): + return True + + return False def append_comment(self, comment: Leaf) -> bool: """Add an inline or standalone comment to the line.""" @@ -2085,13 +2036,20 @@ def visit_factor(self, node: Node) -> Iterator[Line]: yield from self.visit_default(node) def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: - # Check if it's a docstring - if prev_siblings_are( - leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] - ) and is_multiline_string(leaf): - prefix = " " * self.current_line.depth - docstring = fix_docstring(leaf.value[3:-3], prefix) - leaf.value = leaf.value[0:3] + docstring + leaf.value[-3:] + if is_docstring(leaf) and "\\\n" not in leaf.value: + # We're ignoring docstrings with backslash newline escapes because changing + # indentation of those changes the AST representation of the code. + prefix = get_string_prefix(leaf.value) + lead_len = len(prefix) + 3 + tail_len = -3 + indent = " " * 4 * self.current_line.depth + docstring = fix_docstring(leaf.value[lead_len:tail_len], indent) + if docstring: + if leaf.value[lead_len - 1] == docstring[0]: + docstring = " " + docstring + if leaf.value[tail_len + 1] == docstring[-1]: + docstring = docstring + " " + leaf.value = leaf.value[0:lead_len] + docstring + leaf.value[tail_len:] normalize_string_quotes(leaf) yield from self.visit_default(leaf) @@ -2638,10 +2596,7 @@ def make_comment(content: str) -> str: def transform_line( - line: Line, - line_length: int, - normalize_strings: bool, - features: Collection[Feature] = (), + line: Line, mode: Mode, features: Collection[Feature] = () ) -> Iterator[Line]: """Transform a `line`, potentially splitting it into many lines. @@ -2657,7 +2612,7 @@ def transform_line( def init_st(ST: Type[StringTransformer]) -> StringTransformer: """Initialize StringTransformer""" - return ST(line_length, normalize_strings) + return ST(mode.line_length, mode.string_normalization) string_merge = init_st(StringMerger) string_paren_strip = init_st(StringParenStripper) @@ -2668,72 +2623,79 @@ def init_st(ST: Type[StringTransformer]) -> StringTransformer: if ( not line.contains_uncollapsable_type_comments() and not line.should_explode - and not line.is_collection_with_optional_trailing_comma and ( - is_line_short_enough(line, line_length=line_length, line_str=line_str) + is_line_short_enough(line, line_length=mode.line_length, line_str=line_str) or line.contains_unsplittable_type_ignore() ) - and not (line.contains_standalone_comments() and line.inside_brackets) + and not (line.inside_brackets and line.contains_standalone_comments()) ): # Only apply basic string preprocessing, since lines shouldn't be split here. - transformers = [string_merge, string_paren_strip] + if mode.experimental_string_processing: + transformers = [string_merge, string_paren_strip] + else: + transformers = [] elif line.is_def: transformers = [left_hand_split] else: def rhs(line: Line, features: Collection[Feature]) -> Iterator[Line]: - for omit in generate_trailers_to_omit(line, line_length): - lines = list(right_hand_split(line, line_length, features, omit=omit)) - if is_line_short_enough(lines[0], line_length=line_length): + """Wraps calls to `right_hand_split`. + + The calls increasingly `omit` right-hand trailers (bracket pairs with + content), meaning the trailers get glued together to split on another + bracket pair instead. + """ + for omit in generate_trailers_to_omit(line, mode.line_length): + lines = list( + right_hand_split(line, mode.line_length, features, omit=omit) + ) + # Note: this check is only able to figure out if the first line of the + # *current* transformation fits in the line length. This is true only + # for simple cases. All others require running more transforms via + # `transform_line()`. This check doesn't know if those would succeed. + if is_line_short_enough(lines[0], line_length=mode.line_length): yield from lines return # All splits failed, best effort split with no omits. # This mostly happens to multiline strings that are by definition - # reported as not fitting a single line. - # line_length=1 here was historically a bug that somehow became a feature. - # See #762 and #781 for the full story. - yield from right_hand_split(line, line_length=1, features=features) - - if line.inside_brackets: - transformers = [ - string_merge, - string_paren_strip, - delimiter_split, - standalone_comment_split, - string_split, - string_paren_wrap, - rhs, - ] + # reported as not fitting a single line, as well as lines that contain + # trailing commas (those have to be exploded). + yield from right_hand_split( + line, line_length=mode.line_length, features=features + ) + + if mode.experimental_string_processing: + if line.inside_brackets: + transformers = [ + string_merge, + string_paren_strip, + delimiter_split, + standalone_comment_split, + string_split, + string_paren_wrap, + rhs, + ] + else: + transformers = [ + string_merge, + string_paren_strip, + string_split, + string_paren_wrap, + rhs, + ] else: - transformers = [ - string_merge, - string_paren_strip, - string_split, - string_paren_wrap, - rhs, - ] + if line.inside_brackets: + transformers = [delimiter_split, standalone_comment_split, rhs] + else: + transformers = [rhs] for transform in transformers: # We are accumulating lines in `result` because we might want to abort # mission and return the original line in the end, or attempt a different # split altogether. - result: List[Line] = [] try: - for transformed_line in transform(line, features): - if str(transformed_line).strip("\n") == line_str: - raise CannotTransform( - "Line transformer returned an unchanged result" - ) - - result.extend( - transform_line( - transformed_line, - line_length=line_length, - normalize_strings=normalize_strings, - features=features, - ) - ) + result = run_transformer(line, transform, mode, features, line_str=line_str) except CannotTransform: continue else: @@ -2774,6 +2736,7 @@ class StringTransformer(ABC): line_length: int normalize_strings: bool + __name__ = "StringTransformer" @abstractmethod def do_match(self, line: Line) -> TMatchResult: @@ -3020,7 +2983,7 @@ def __remove_backslash_line_continuation_chars( ) new_line = line.clone() - new_line.comments = line.comments + new_line.comments = line.comments.copy() append_leaves(new_line, line, LL) new_string_leaf = new_line.leaves[string_idx] @@ -3238,7 +3201,9 @@ class StringParenStripper(StringTransformer): Requirements: The line contains a string which is surrounded by parentheses and: - The target string is NOT the only argument to a function call). - - The RPAR is NOT followed by an attribute access (i.e. a dot). + - If the target string contains a PERCENT, the brackets are not + preceeded or followed by an operator with higher precedence than + PERCENT. Transformations: The parentheses mentioned in the 'Requirements' section are stripped. @@ -3281,14 +3246,51 @@ def do_match(self, line: Line) -> TMatchResult: string_parser = StringParser() next_idx = string_parser.parse(LL, string_idx) + # if the leaves in the parsed string include a PERCENT, we need to + # make sure the initial LPAR is NOT preceded by an operator with + # higher or equal precedence to PERCENT + if is_valid_index(idx - 2): + # mypy can't quite follow unless we name this + before_lpar = LL[idx - 2] + if token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} and ( + ( + before_lpar.type + in { + token.STAR, + token.AT, + token.SLASH, + token.DOUBLESLASH, + token.PERCENT, + token.TILDE, + token.DOUBLESTAR, + token.AWAIT, + token.LSQB, + token.LPAR, + } + ) + or ( + # only unary PLUS/MINUS + before_lpar.parent + and before_lpar.parent.type == syms.factor + and (before_lpar.type in {token.PLUS, token.MINUS}) + ) + ): + continue + # Should be followed by a non-empty RPAR... if ( is_valid_index(next_idx) and LL[next_idx].type == token.RPAR and not is_empty_rpar(LL[next_idx]) ): - # That RPAR should NOT be followed by a '.' symbol. - if is_valid_index(next_idx + 1) and LL[next_idx + 1].type == token.DOT: + # That RPAR should NOT be followed by anything with higher + # precedence than PERCENT + if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in { + token.DOUBLESTAR, + token.LSQB, + token.LPAR, + token.DOT, + }: continue return Ok(string_idx) @@ -3309,7 +3311,6 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: new_line = line.clone() new_line.comments = line.comments.copy() - append_leaves(new_line, line, LL[: string_idx - 1]) string_leaf = Leaf(token.STRING, LL[string_idx].value) @@ -3318,7 +3319,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: new_line.append(string_leaf) append_leaves( - new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :], + new_line, line, LL[string_idx + 1 : rpar_idx] + LL[rpar_idx + 1 :] ) LL[rpar_idx].remove() @@ -4594,8 +4595,6 @@ def append_leaves(new_line: Line, old_line: Line, leaves: List[Leaf]) -> None: set(@leaves) is a subset of set(@old_line.leaves). """ for old_leaf in leaves: - assert old_leaf in old_line.leaves - new_leaf = Leaf(old_leaf.type, old_leaf.value) replace_child(old_leaf, new_leaf) new_line.append(new_leaf) @@ -4755,8 +4754,7 @@ def right_hand_split( tail = bracket_split_build_line(tail_leaves, line, opening_bracket) bracket_split_succeeded_or_raise(head, body, tail) if ( - # the body shouldn't be exploded - not body.should_explode + Feature.FORCE_OPTIONAL_PARENTHESES not in features # the opening bracket is an optional paren and opening_bracket.type == token.LPAR and not opening_bracket.value @@ -4769,7 +4767,7 @@ def right_hand_split( # there are no standalone comments in the body and not body.contains_standalone_comments(0) # and we can actually remove the parens - and can_omit_invisible_parens(body, line_length) + and can_omit_invisible_parens(body, line_length, omit_on_explode=omit) ): omit = {id(closing_bracket), *omit} try: @@ -4855,7 +4853,8 @@ def bracket_split_build_line( continue if leaves[i].type != token.COMMA: - leaves.insert(i + 1, Leaf(token.COMMA, ",")) + new_comma = Leaf(token.COMMA, ",") + leaves.insert(i + 1, new_comma) break # Populate the line @@ -4863,8 +4862,8 @@ def bracket_split_build_line( result.append(leaf, preformatted=True) for comment_after in original.comments_after(leaf): result.append(comment_after, preformatted=True) - if is_body: - result.should_explode = should_explode(result, opening_bracket) + if is_body and should_split_body_explode(result, opening_bracket): + result.should_explode = True return result @@ -4949,7 +4948,8 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: and current_line.leaves[-1].type != token.COMMA and current_line.leaves[-1].type != STANDALONE_COMMENT ): - current_line.append(Leaf(token.COMMA, ",")) + new_comma = Leaf(token.COMMA, ",") + current_line.append(new_comma) yield current_line @@ -5571,24 +5571,63 @@ def ensure_visible(leaf: Leaf) -> None: leaf.value = ")" -def should_explode(line: Line, opening_bracket: Leaf) -> bool: - """Should `line` immediately be split with `delimiter_split()` after RHS?""" +def should_split_body_explode(line: Line, opening_bracket: Leaf) -> bool: + """Should `line` be immediately split with `delimiter_split()` after RHS?""" - if not ( - opening_bracket.parent - and opening_bracket.parent.type in {syms.atom, syms.import_from} - and opening_bracket.value in "[{(" - ): + if not (opening_bracket.parent and opening_bracket.value in "[{("): return False + # We're essentially checking if the body is delimited by commas and there's more + # than one of them (we're excluding the trailing comma and if the delimiter priority + # is still commas, that means there's more). + exclude = set() + trailing_comma = False try: last_leaf = line.leaves[-1] - exclude = {id(last_leaf)} if last_leaf.type == token.COMMA else set() + if last_leaf.type == token.COMMA: + trailing_comma = True + exclude.add(id(last_leaf)) max_priority = line.bracket_tracker.max_delimiter_priority(exclude=exclude) except (IndexError, ValueError): return False - return max_priority == COMMA_PRIORITY + return max_priority == COMMA_PRIORITY and ( + trailing_comma + # always explode imports + or opening_bracket.parent.type in {syms.atom, syms.import_from} + ) + + +def is_one_tuple_between(opening: Leaf, closing: Leaf, leaves: List[Leaf]) -> bool: + """Return True if content between `opening` and `closing` looks like a one-tuple.""" + if opening.type != token.LPAR and closing.type != token.RPAR: + return False + + depth = closing.bracket_depth + 1 + for _opening_index, leaf in enumerate(leaves): + if leaf is opening: + break + + else: + raise LookupError("Opening paren not found in `leaves`") + + commas = 0 + _opening_index += 1 + for leaf in leaves[_opening_index:]: + if leaf is closing: + break + + bracket_depth = leaf.bracket_depth + if bracket_depth == depth and leaf.type == token.COMMA: + commas += 1 + if leaf.parent and leaf.parent.type in { + syms.arglist, + syms.typedargslist, + }: + commas += 1 + break + + return commas < 2 def get_features_used(node: Node) -> Set[Feature]: @@ -5655,11 +5694,13 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf a preceding closing bracket fits in one line. Yielded sets are cumulative (contain results of previous yields, too). First - set is empty. + set is empty, unless the line should explode, in which case bracket pairs until + the one that needs to explode are omitted. """ omit: Set[LeafID] = set() - yield omit + if not line.should_explode: + yield omit length = 4 * line.depth opening_bracket: Optional[Leaf] = None @@ -5678,9 +5719,23 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf if leaf is opening_bracket: opening_bracket = None elif leaf.type in CLOSING_BRACKETS: + prev = line.leaves[index - 1] if index > 0 else None + if ( + line.should_explode + and prev + and prev.type == token.COMMA + and not is_one_tuple_between( + leaf.opening_bracket, leaf, line.leaves + ) + ): + # Never omit bracket pairs with trailing commas. + # We need to explode on those. + break + inner_brackets.add(id(leaf)) elif leaf.type in CLOSING_BRACKETS: - if index > 0 and line.leaves[index - 1].type in OPENING_BRACKETS: + prev = line.leaves[index - 1] if index > 0 else None + if prev and prev.type in OPENING_BRACKETS: # Empty brackets would fail a split so treat them as "inner" # brackets (e.g. only add them to the `omit` set if another # pair of brackets was good enough. @@ -5693,6 +5748,16 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf inner_brackets.clear() yield omit + if ( + line.should_explode + and prev + and prev.type == token.COMMA + and not is_one_tuple_between(leaf.opening_bracket, leaf, line.leaves) + ): + # Never omit bracket pairs with trailing commas. + # We need to explode on those. + break + if leaf.value: opening_bracket = leaf.opening_bracket closing_bracket = leaf @@ -5759,16 +5824,40 @@ def get_gitignore(root: Path) -> PathSpec: return PathSpec.from_lines("gitwildmatch", lines) +def normalize_path_maybe_ignore( + path: Path, root: Path, report: "Report" +) -> Optional[str]: + """Normalize `path`. May return `None` if `path` was ignored. + + `report` is where "path ignored" output goes. + """ + try: + normalized_path = path.resolve().relative_to(root).as_posix() + except OSError as e: + report.path_ignored(path, f"cannot be read because {e}") + return None + + except ValueError: + if path.is_symlink(): + report.path_ignored(path, f"is a symbolic link that points outside {root}") + return None + + raise + + return normalized_path + + def gen_python_files( paths: Iterable[Path], root: Path, include: Optional[Pattern[str]], - exclude_regexes: Iterable[Pattern[str]], + exclude: Pattern[str], + force_exclude: Optional[Pattern[str]], report: "Report", gitignore: PathSpec, ) -> Iterator[Path]: """Generate all files under `path` whose paths are not excluded by the - `exclude` regex, but are included by the `include` regex. + `exclude_regex` or `force_exclude` regexes, but are included by the `include` regex. Symbolic links pointing outside of the `root` directory are ignored. @@ -5776,43 +5865,41 @@ def gen_python_files( """ assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}" for child in paths: - # Then ignore with `exclude` option. - try: - normalized_path = child.resolve().relative_to(root).as_posix() - except OSError as e: - report.path_ignored(child, f"cannot be read because {e}") + normalized_path = normalize_path_maybe_ignore(child, root, report) + if normalized_path is None: continue - except ValueError: - if child.is_symlink(): - report.path_ignored( - child, f"is a symbolic link that points outside {root}" - ) - continue - - raise # First ignore files matching .gitignore if gitignore.match_file(normalized_path): report.path_ignored(child, "matches the .gitignore file content") continue + # Then ignore with `--exclude` and `--force-exclude` options. normalized_path = "/" + normalized_path if child.is_dir(): normalized_path += "/" - is_excluded = False - for exclude in exclude_regexes: - exclude_match = exclude.search(normalized_path) if exclude else None - if exclude_match and exclude_match.group(0): - report.path_ignored(child, "matches the --exclude regular expression") - is_excluded = True - break - if is_excluded: + exclude_match = exclude.search(normalized_path) if exclude else None + if exclude_match and exclude_match.group(0): + report.path_ignored(child, "matches the --exclude regular expression") + continue + + force_exclude_match = ( + force_exclude.search(normalized_path) if force_exclude else None + ) + if force_exclude_match and force_exclude_match.group(0): + report.path_ignored(child, "matches the --force-exclude regular expression") continue if child.is_dir(): yield from gen_python_files( - child.iterdir(), root, include, exclude_regexes, report, gitignore + child.iterdir(), + root, + include, + exclude, + force_exclude, + report, + gitignore, ) elif child.is_file(): @@ -5825,8 +5912,8 @@ def gen_python_files( def find_project_root(srcs: Iterable[str]) -> Path: """Return a directory containing .git, .hg, or pyproject.toml. - That directory can be one of the directories passed in `srcs` or their - common parent. + That directory will be a common parent of all files and directories + passed in `srcs`. If no directory in the tree contains a marker that would specify it's the project root, the root of the file system is returned. @@ -5834,11 +5921,20 @@ def find_project_root(srcs: Iterable[str]) -> Path: if not srcs: return Path("/").resolve() - common_base = min(Path(src).resolve() for src in srcs) - if common_base.is_dir(): - # Append a fake file so `parents` below returns `common_base_dir`, too. - common_base /= "fake-file" - for directory in common_base.parents: + path_srcs = [Path(Path.cwd(), src).resolve() for src in srcs] + + # A list of lists of parents for each 'src'. 'src' is included as a + # "parent" of itself if it is a directory + src_parents = [ + list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs + ] + + common_base = max( + set.intersection(*(set(parents) for parents in src_parents)), + key=lambda path: path.parts, + ) + + for directory in (common_base, *common_base.parents): if (directory / ".git").exists(): return directory @@ -6023,7 +6119,7 @@ def _stringify_ast( and field == "value" and isinstance(value, str) ): - normalized = re.sub(r" *\n[ \t]+", "\n ", value).strip() + normalized = re.sub(r" *\n[ \t]*", "\n", value).strip() else: normalized = value yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}" @@ -6067,6 +6163,7 @@ def assert_stable(src: str, dst: str, mode: Mode) -> None: newdst = format_str(dst, mode=mode) if dst != newdst: log = dump_to_file( + str(mode), diff(src, dst, "source", "first pass"), diff(dst, newdst, "first pass", "second pass"), ) @@ -6243,7 +6340,11 @@ def can_be_split(line: Line) -> bool: return True -def can_omit_invisible_parens(line: Line, line_length: int) -> bool: +def can_omit_invisible_parens( + line: Line, + line_length: int, + omit_on_explode: Collection[LeafID] = (), +) -> bool: """Does `line` have a shape safe to reformat without optional parens around it? Returns True for only a subset of potentially nice looking formattings but @@ -6266,37 +6367,27 @@ def can_omit_invisible_parens(line: Line, line_length: int) -> bool: assert len(line.leaves) >= 2, "Stranded delimiter" - first = line.leaves[0] - second = line.leaves[1] - penultimate = line.leaves[-2] - last = line.leaves[-1] - # With a single delimiter, omit if the expression starts or ends with # a bracket. + first = line.leaves[0] + second = line.leaves[1] if first.type in OPENING_BRACKETS and second.type not in CLOSING_BRACKETS: - remainder = False - length = 4 * line.depth - for _index, leaf, leaf_length in enumerate_with_length(line): - if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first: - remainder = True - if remainder: - length += leaf_length - if length > line_length: - break - - if leaf.type in OPENING_BRACKETS: - # There are brackets we can further split on. - remainder = False - - else: - # checked the entire string and line length wasn't exceeded - if len(line.leaves) == _index + 1: - return True + if _can_omit_opening_paren(line, first=first, line_length=line_length): + return True # Note: we are not returning False here because a line might have *both* # a leading opening bracket and a trailing closing bracket. If the # opening bracket doesn't match our rule, maybe the closing will. + penultimate = line.leaves[-2] + last = line.leaves[-1] + if line.should_explode: + try: + penultimate, last = last_two_except(line.leaves, omit=omit_on_explode) + except LookupError: + # Turns out we'd omit everything. We cannot skip the optional parentheses. + return False + if ( last.type == token.RPAR or last.type == token.RBRACE @@ -6317,21 +6408,120 @@ def can_omit_invisible_parens(line: Line, line_length: int) -> bool: # unnecessary. return True - length = 4 * line.depth - seen_other_brackets = False - for _index, leaf, leaf_length in enumerate_with_length(line): + if line.should_explode and penultimate.type == token.COMMA: + # The rightmost non-omitted bracket pair is the one we want to explode on. + return True + + if _can_omit_closing_paren(line, last=last, line_length=line_length): + return True + + return False + + +def _can_omit_opening_paren(line: Line, *, first: Leaf, line_length: int) -> bool: + """See `can_omit_invisible_parens`.""" + remainder = False + length = 4 * line.depth + _index = -1 + for _index, leaf, leaf_length in enumerate_with_length(line): + if leaf.type in CLOSING_BRACKETS and leaf.opening_bracket is first: + remainder = True + if remainder: length += leaf_length - if leaf is last.opening_bracket: - if seen_other_brackets or length <= line_length: - return True + if length > line_length: + break - elif leaf.type in OPENING_BRACKETS: + if leaf.type in OPENING_BRACKETS: # There are brackets we can further split on. - seen_other_brackets = True + remainder = False + + else: + # checked the entire string and line length wasn't exceeded + if len(line.leaves) == _index + 1: + return True + + return False + + +def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool: + """See `can_omit_invisible_parens`.""" + length = 4 * line.depth + seen_other_brackets = False + for _index, leaf, leaf_length in enumerate_with_length(line): + length += leaf_length + if leaf is last.opening_bracket: + if seen_other_brackets or length <= line_length: + return True + + elif leaf.type in OPENING_BRACKETS: + # There are brackets we can further split on. + seen_other_brackets = True return False +def last_two_except(leaves: List[Leaf], omit: Collection[LeafID]) -> Tuple[Leaf, Leaf]: + """Return (penultimate, last) leaves skipping brackets in `omit` and contents.""" + stop_after = None + last = None + for leaf in reversed(leaves): + if stop_after: + if leaf is stop_after: + stop_after = None + continue + + if last: + return leaf, last + + if id(leaf) in omit: + stop_after = leaf.opening_bracket + else: + last = leaf + else: + raise LookupError("Last two leaves were also skipped") + + +def run_transformer( + line: Line, + transform: Transformer, + mode: Mode, + features: Collection[Feature], + *, + line_str: str = "", +) -> List[Line]: + if not line_str: + line_str = line_to_string(line) + result: List[Line] = [] + for transformed_line in transform(line, features): + if str(transformed_line).strip("\n") == line_str: + raise CannotTransform("Line transformer returned an unchanged result") + + result.extend(transform_line(transformed_line, mode=mode, features=features)) + + if not ( + transform.__name__ == "rhs" + and line.bracket_tracker.invisible + and not any(bracket.value for bracket in line.bracket_tracker.invisible) + and not line.contains_multiline_strings() + and not result[0].contains_uncollapsable_type_comments() + and not result[0].contains_unsplittable_type_ignore() + and not is_line_short_enough(result[0], line_length=mode.line_length) + ): + return result + + line_copy = line.clone() + append_leaves(line_copy, line, line.leaves) + features_fop = set(features) | {Feature.FORCE_OPTIONAL_PARENTHESES} + second_opinion = run_transformer( + line_copy, transform, mode, features_fop, line_str=line_str + ) + if all( + is_line_short_enough(ln, line_length=mode.line_length) for ln in second_opinion + ): + result = second_opinion + return result + + def get_cache_file(mode: Mode) -> Path: return CACHE_DIR / f"cache.{mode.get_cache_key()}.pickle" @@ -6417,6 +6607,26 @@ def patched_main() -> None: main() +def is_docstring(leaf: Leaf) -> bool: + if not is_multiline_string(leaf): + # For the purposes of docstring re-indentation, we don't need to do anything + # with single-line docstrings. + return False + + if prev_siblings_are( + leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt] + ): + return True + + # Multiline docstring on the same line as the `def`. + if prev_siblings_are(leaf.parent, [syms.parameters, token.COLON, syms.simple_stmt]): + # `syms.parameters` is only used in funcdefs and async_funcdefs in the Python + # grammar. We're safe to return True without further checks. + return True + + return False + + def fix_docstring(docstring: str, prefix: str) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation if not docstring: @@ -6440,7 +6650,6 @@ def fix_docstring(docstring: str, prefix: str) -> str: trimmed.append(prefix + stripped_line) else: trimmed.append("") - # Return a single string: return "\n".join(trimmed) diff --git a/src/black_primer/primer.json b/src/black_primer/primer.json index f5cc3fdf931..546f47782cd 100644 --- a/src/black_primer/primer.json +++ b/src/black_primer/primer.json @@ -30,7 +30,7 @@ "py_versions": ["all"] }, "django": { - "disabled_reason": "black --check --diff returned 123", + "disabled_reason": "black --check --diff returned 123 on two files", "disabled": true, "cli_arguments": [], "expect_formatting_changes": true, @@ -53,10 +53,10 @@ "py_versions": ["all"] }, "pandas": { - "disabled_reason": "black --check --diff returned 123", + "disabled_reason": "black --check --diff returned 123 on one file", "disabled": true, "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/pandas-dev/pandas.git", "long_checkout": false, "py_versions": ["all"] @@ -83,10 +83,8 @@ "py_versions": ["all"] }, "pytest": { - "disabled_reason": "black --check --diff returned 123", - "disabled": true, "cli_arguments": [], - "expect_formatting_changes": false, + "expect_formatting_changes": true, "git_clone_url": "https://github.com/pytest-dev/pytest.git", "long_checkout": false, "py_versions": ["all"] diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index 052c94883cf..81940f78f0f 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -128,7 +128,7 @@ def parse_stream(self, stream: IO[Text], debug: bool = False) -> NL: return self.parse_stream_raw(stream, debug) def parse_file( - self, filename: Path, encoding: Optional[Text] = None, debug: bool = False, + self, filename: Path, encoding: Optional[Text] = None, debug: bool = False ) -> NL: """Parse a file and return the syntax tree.""" with io.open(filename, "r", encoding=encoding) as stream: diff --git a/tests/data/cantfit.py b/tests/data/cantfit.py index ef9b78e09a9..0849374f776 100644 --- a/tests/data/cantfit.py +++ b/tests/data/cantfit.py @@ -67,11 +67,15 @@ normal_name = ( but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying() ) -normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying( - arg1, arg2, arg3 +normal_name = ( + but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying( + arg1, arg2, arg3 + ) ) -normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying( - [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 +normal_name = ( + but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying( + [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 + ) ) # long arguments normal_name = normal_function_name( diff --git a/tests/data/collections.py b/tests/data/collections.py index ebe8d3c5200..68431665211 100644 --- a/tests/data/collections.py +++ b/tests/data/collections.py @@ -2,18 +2,18 @@ from . import A, B, C -# unwraps +# keeps existing trailing comma from foo import ( bar, ) -# stays wrapped +# also keeps existing structure from foo import ( baz, qux, ) -# as doesn't get confusing when unwrapped +# `as` works as well from foo import ( xyzzy as magic, ) @@ -77,17 +77,21 @@ from . import A, B, C -# unwraps -from foo import bar +# keeps existing trailing comma +from foo import ( + bar, +) -# stays wrapped +# also keeps existing structure from foo import ( baz, qux, ) -# as doesn't get confusing when unwrapped -from foo import xyzzy as magic +# `as` works as well +from foo import ( + xyzzy as magic, +) a = { 1, @@ -151,11 +155,20 @@ if True: ec2client.get_waiter("instance_stopped").wait( - InstanceIds=[instance.id], WaiterConfig={"Delay": 5,} + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, ) ec2client.get_waiter("instance_stopped").wait( - InstanceIds=[instance.id], WaiterConfig={"Delay": 5,}, + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, ) ec2client.get_waiter("instance_stopped").wait( - InstanceIds=[instance.id], WaiterConfig={"Delay": 5,}, + InstanceIds=[instance.id], + WaiterConfig={ + "Delay": 5, + }, ) diff --git a/tests/data/comments2.py b/tests/data/comments2.py index 89c29104bd8..221cb3fe143 100644 --- a/tests/data/comments2.py +++ b/tests/data/comments2.py @@ -316,7 +316,13 @@ def inline_comments_in_brackets_ruin_everything(): ) -CONFIG_FILES = [CONFIG_FILE,] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final +CONFIG_FILES = ( + [ + CONFIG_FILE, + ] + + SHARED_CONFIG_FILES + + USER_CONFIG_FILES +) # type: Final class Test: diff --git a/tests/data/comments7.py b/tests/data/comments7.py index 436df1a2a41..0e2bd35bf55 100644 --- a/tests/data/comments7.py +++ b/tests/data/comments7.py @@ -22,6 +22,12 @@ # resolve_to_config_type, # DEFAULT_TYPE_ATTRIBUTES, ) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component # DRY +) result = 1 # look ma, no comment migration xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -46,6 +52,26 @@ def func(): 0.0789, a[-1], # type: ignore ) + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0789, + a[-1] # type: ignore + ) + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + a[-1] # type: ignore + ) # The type: ignore exception only applies to line length, not # other types of formatting. @@ -55,6 +81,54 @@ def func(): ) +class C: + @pytest.mark.parametrize( + ("post_data", "message"), + [ + # metadata_version errors. + ( + {}, + "None is an invalid value for Metadata-Version. Error: This field is" + " required. see" + " https://packaging.python.org/specifications/core-metadata" + ), + ( + {"metadata_version": "-1"}, + "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" + " Version see" + " https://packaging.python.org/specifications/core-metadata" + ), + # name errors. + ( + {"metadata_version": "1.2"}, + "'' is an invalid value for Name. Error: This field is required. see" + " https://packaging.python.org/specifications/core-metadata" + ), + ( + {"metadata_version": "1.2", "name": "foo-"}, + "'foo-' is an invalid value for Name. Error: Must start and end with a" + " letter or numeral and contain only ascii numeric and '.', '_' and" + " '-'. see https://packaging.python.org/specifications/core-metadata" + ), + # version errors. + ( + {"metadata_version": "1.2", "name": "example"}, + "'' is an invalid value for Version. Error: This field is required. see" + " https://packaging.python.org/specifications/core-metadata" + ), + ( + {"metadata_version": "1.2", "name": "example", "version": "dog"}, + "'dog' is an invalid value for Version. Error: Must start and end with" + " a letter or numeral and contain only ascii numeric and '.', '_' and" + " '-'. see https://packaging.python.org/specifications/core-metadata" + ) + ] + ) + def test_fails_invalid_post_data( + self, pyramid_config, db_request, post_data, message + ): + ... + # output from .config import ( @@ -81,6 +155,12 @@ def func(): # resolve_to_config_type, # DEFAULT_TYPE_ATTRIBUTES, ) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent, # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component, # DRY +) result = 1 # look ma, no comment migration xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx @@ -97,7 +177,27 @@ def func(): def func(): - c = call(0.0123, 0.0456, 0.0789, 0.0123, 0.0789, a[-1],) # type: ignore + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0789, + a[-1], # type: ignore + ) + c = call(0.0123, 0.0456, 0.0789, 0.0123, 0.0789, a[-1]) # type: ignore + c = call( + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + 0.0123, + 0.0456, + 0.0789, + a[-1], # type: ignore + ) # The type: ignore exception only applies to line length, not # other types of formatting. @@ -115,3 +215,52 @@ def func(): "aaaaaaaa", "aaaaaaaa", ) + + +class C: + @pytest.mark.parametrize( + ("post_data", "message"), + [ + # metadata_version errors. + ( + {}, + "None is an invalid value for Metadata-Version. Error: This field is" + " required. see" + " https://packaging.python.org/specifications/core-metadata", + ), + ( + {"metadata_version": "-1"}, + "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" + " Version see" + " https://packaging.python.org/specifications/core-metadata", + ), + # name errors. + ( + {"metadata_version": "1.2"}, + "'' is an invalid value for Name. Error: This field is required. see" + " https://packaging.python.org/specifications/core-metadata", + ), + ( + {"metadata_version": "1.2", "name": "foo-"}, + "'foo-' is an invalid value for Name. Error: Must start and end with a" + " letter or numeral and contain only ascii numeric and '.', '_' and" + " '-'. see https://packaging.python.org/specifications/core-metadata", + ), + # version errors. + ( + {"metadata_version": "1.2", "name": "example"}, + "'' is an invalid value for Version. Error: This field is required. see" + " https://packaging.python.org/specifications/core-metadata", + ), + ( + {"metadata_version": "1.2", "name": "example", "version": "dog"}, + "'dog' is an invalid value for Version. Error: Must start and end with" + " a letter or numeral and contain only ascii numeric and '.', '_' and" + " '-'. see https://packaging.python.org/specifications/core-metadata", + ), + ], + ) + def test_fails_invalid_post_data( + self, pyramid_config, db_request, post_data, message + ): + ... \ No newline at end of file diff --git a/tests/data/composition_no_trailing_comma.py b/tests/data/composition_no_trailing_comma.py new file mode 100644 index 00000000000..f17b89dea8d --- /dev/null +++ b/tests/data/composition_no_trailing_comma.py @@ -0,0 +1,367 @@ +class C: + def test(self) -> None: + with patch("black.out", print): + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." + ) + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 1 file left unchanged, 1 file failed to" + " reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", + ) + for i in (a,): + if ( + # Rule 1 + i % 2 == 0 + # Rule 2 + and i % 3 == 0 + ): + while ( + # Just a comment + call() + # Another + ): + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, + max_items_to_push=num_items, + batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE + ).push( + # Only send the first n items. + items=items[:num_items] + ) + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) + ) + + def omitting_trailers(self) -> None: + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] + assignment = ( + some.rather.elaborate.rule() and another.rule.ending_with.index[123] + ) + + def easy_asserts(self) -> None: + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } == expected, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + }, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } + + def tricky_asserts(self) -> None: + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } == expected( + value, is_going_to_be="too long to fit in a single line", srsly=True + ), "Not what we expected" + + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } == expected, ( + "Not what we expected and the message is too long to fit in one line" + ) + + assert expected( + value, is_going_to_be="too long to fit in a single line", srsly=True + ) == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + }, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + }, ( + "Not what we expected and the message is too long to fit in one line" + " because it's too long" + ) + + dis_c_instance_method = """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) + 6 LOAD_FAST 0 (self) + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE + """ % ( + _C.__init__.__code__.co_firstlineno + 1, + ) + + assert ( + expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect + == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9 + } + ) + + + +# output + +class C: + def test(self) -> None: + with patch("black.out", print): + self.assertEqual( + unstyle(str(report)), "1 file reformatted, 1 file failed to reformat." + ) + self.assertEqual( + unstyle(str(report)), + "1 file reformatted, 1 file left unchanged, 1 file failed to reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 1 file left unchanged, 1 file failed to" + " reformat.", + ) + self.assertEqual( + unstyle(str(report)), + "2 files reformatted, 2 files left unchanged, 2 files failed to" + " reformat.", + ) + for i in (a,): + if ( + # Rule 1 + i % 2 == 0 + # Rule 2 + and i % 3 == 0 + ): + while ( + # Just a comment + call() + # Another + ): + print(i) + xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( + push_manager=context.request.resource_manager, + max_items_to_push=num_items, + batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, + ).push( + # Only send the first n items. + items=items[:num_items] + ) + return ( + 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' + % (test.name, test.filename, lineno, lname, err) + ) + + def omitting_trailers(self) -> None: + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex] + get_collection( + hey_this_is_a_very_long_call, it_has_funny_attributes, really=True + )[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex] + d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][ + 22 + ] + assignment = ( + some.rather.elaborate.rule() and another.rule.ending_with.index[123] + ) + + def easy_asserts(self) -> None: + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } == expected, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + }, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } + + def tricky_asserts(self) -> None: + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } == expected( + value, is_going_to_be="too long to fit in a single line", srsly=True + ), "Not what we expected" + + assert { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } == expected, ( + "Not what we expected and the message is too long to fit in one line" + ) + + assert expected( + value, is_going_to_be="too long to fit in a single line", srsly=True + ) == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + }, "Not what we expected" + + assert expected == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + }, ( + "Not what we expected and the message is too long to fit in one line" + " because it's too long" + ) + + dis_c_instance_method = """\ + %3d 0 LOAD_FAST 1 (x) + 2 LOAD_CONST 1 (1) + 4 COMPARE_OP 2 (==) + 6 LOAD_FAST 0 (self) + 8 STORE_ATTR 0 (x) + 10 LOAD_CONST 0 (None) + 12 RETURN_VALUE + """ % ( + _C.__init__.__code__.co_firstlineno + 1, + ) + + assert ( + expectedexpectedexpectedexpectedexpectedexpectedexpectedexpectedexpect + == { + key1: value1, + key2: value2, + key3: value3, + key4: value4, + key5: value5, + key6: value6, + key7: value7, + key8: value8, + key9: value9, + } + ) diff --git a/tests/data/docstring.py b/tests/data/docstring.py index f5adeb7bb7b..2d3d73a101c 100644 --- a/tests/data/docstring.py +++ b/tests/data/docstring.py @@ -46,7 +46,7 @@ def zort(): def poit(): """ - Lorem ipsum dolor sit amet. + Lorem ipsum dolor sit amet. Consectetur adipiscing elit: - sed do eiusmod tempor incididunt ut labore @@ -58,6 +58,14 @@ def poit(): pass +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + def over_indent(): """ This has a shallow indent @@ -73,6 +81,35 @@ def single_line(): """ pass + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + # output class MyClass: @@ -136,6 +173,14 @@ def poit(): pass +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + def over_indent(): """ This has a shallow indent @@ -148,3 +193,33 @@ def over_indent(): def single_line(): """But with a newline after it!""" pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + ''' + "hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" \ No newline at end of file diff --git a/tests/data/expression.diff b/tests/data/expression.diff index f47ee1c6d2c..684f92cd3b7 100644 --- a/tests/data/expression.diff +++ b/tests/data/expression.diff @@ -130,15 +130,21 @@ call(**self.screen_kwargs) call(b, **self.screen_kwargs) lukasz.langa.pl -@@ -94,23 +127,25 @@ +@@ -94,26 +127,29 @@ 1.0 .real ....__class__ list[str] dict[str, int] tuple[str, ...] ++tuple[str, int, float, dict[str, int]] + tuple[ +- str, int, float, dict[str, int] +-] -tuple[str, int, float, dict[str, int],] -+tuple[ -+ str, int, float, dict[str, int], ++ str, ++ int, ++ float, ++ dict[str, int], +] very_long_variable_name_filters: t.List[ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], @@ -160,7 +166,7 @@ slice[0:1:2] slice[:] slice[:-1] -@@ -134,112 +169,170 @@ +@@ -137,113 +173,180 @@ numpy[-(c + 1) :, d] numpy[:, l[-2]] numpy[:, ::-1] @@ -200,6 +206,7 @@ g = 1, *"ten" -what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set(vars_to_remove) -what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(vars_to_remove) +-result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc()).all() -result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc(),).all() +what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set( + vars_to_remove @@ -212,7 +219,17 @@ + .filter( + models.Customer.account_id == account_id, models.Customer.email == email_address + ) -+ .order_by(models.Customer.id.asc(),) ++ .order_by(models.Customer.id.asc()) ++ .all() ++) ++result = ( ++ session.query(models.Customer.id) ++ .filter( ++ models.Customer.account_id == account_id, models.Customer.email == email_address ++ ) ++ .order_by( ++ models.Customer.id.asc(), ++ ) + .all() +) Ø = set() diff --git a/tests/data/expression.py b/tests/data/expression.py index 6a04db8b1c4..8e63bdcdf9b 100644 --- a/tests/data/expression.py +++ b/tests/data/expression.py @@ -96,6 +96,9 @@ list[str] dict[str, int] tuple[str, ...] +tuple[ + str, int, float, dict[str, int] +] tuple[str, int, float, dict[str, int],] very_long_variable_name_filters: t.List[ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], @@ -157,6 +160,7 @@ g = 1, *"ten" what_is_up_with_those_new_coord_names = (coord_names + set(vars_to_create)) + set(vars_to_remove) what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(vars_to_remove) +result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc()).all() result = session.query(models.Customer.id).filter(models.Customer.account_id == account_id, models.Customer.email == email_address).order_by(models.Customer.id.asc(),).all() Ø = set() authors.łukasz.say_thanks() @@ -379,8 +383,12 @@ async def f(): list[str] dict[str, int] tuple[str, ...] +tuple[str, int, float, dict[str, int]] tuple[ - str, int, float, dict[str, int], + str, + int, + float, + dict[str, int], ] very_long_variable_name_filters: t.List[ t.Tuple[str, t.Union[str, t.List[t.Optional[str]]]], @@ -459,7 +467,17 @@ async def f(): .filter( models.Customer.account_id == account_id, models.Customer.email == email_address ) - .order_by(models.Customer.id.asc(),) + .order_by(models.Customer.id.asc()) + .all() +) +result = ( + session.query(models.Customer.id) + .filter( + models.Customer.account_id == account_id, models.Customer.email == email_address + ) + .order_by( + models.Customer.id.asc(), + ) .all() ) Ø = set() diff --git a/tests/data/fmtonoff4.py b/tests/data/fmtonoff4.py index 54673c06b2d..4ca707965ad 100644 --- a/tests/data/fmtonoff4.py +++ b/tests/data/fmtonoff4.py @@ -25,7 +25,12 @@ def f(): @test( - [1, 2, 3, 4,] + [ + 1, + 2, + 3, + 4, + ] ) def f(): pass diff --git a/tests/data/function.py b/tests/data/function.py index 51234a1e9b4..2d642c8731b 100644 --- a/tests/data/function.py +++ b/tests/data/function.py @@ -230,7 +230,10 @@ def trailing_comma(): } -def f(a, **kwargs,) -> A: +def f( + a, + **kwargs, +) -> A: return ( yield from A( very_long_argument_name1=very_long_value_for_the_argument, diff --git a/tests/data/function2.py b/tests/data/function2.py index a6773d429cd..cfc259ea7bd 100644 --- a/tests/data/function2.py +++ b/tests/data/function2.py @@ -25,7 +25,10 @@ def inner(): # output -def f(a, **kwargs,) -> A: +def f( + a, + **kwargs, +) -> A: with cache_dir(): if something: result = CliRunner().invoke( diff --git a/tests/data/function_trailing_comma.py b/tests/data/function_trailing_comma.py index fcd81ad7d96..d15459cbeb5 100644 --- a/tests/data/function_trailing_comma.py +++ b/tests/data/function_trailing_comma.py @@ -1,25 +1,88 @@ def f(a,): - ... + d = {'key': 'value',} + tup = (1,) + +def f2(a,b,): + d = {'key': 'value', 'key2': 'value2',} + tup = (1,2,) def f(a:int=1,): - ... + call(arg={'explode': 'this',}) + call2(arg=[1,2,3],) + x = { + "a": 1, + "b": 2, + }["a"] + if a == {"a": 1,"b": 2,"c": 3,"d": 4,"e": 5,"f": 6,"g": 7,"h": 8,}["a"]: + pass def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ]: - pass + json = {"k": {"k2": {"k3": [1,]}}} # output -def f(a,): - ... +def f( + a, +): + d = { + "key": "value", + } + tup = (1,) + + +def f2( + a, + b, +): + d = { + "key": "value", + "key2": "value2", + } + tup = ( + 1, + 2, + ) -def f(a: int = 1,): - ... +def f( + a: int = 1, +): + call( + arg={ + "explode": "this", + } + ) + call2( + arg=[1, 2, 3], + ) + x = { + "a": 1, + "b": 2, + }["a"] + if a == { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6, + "g": 7, + "h": 8, + }["a"]: + pass def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ]: - pass + json = { + "k": { + "k2": { + "k3": [ + 1, + ] + } + } + } \ No newline at end of file diff --git a/tests/data/import_spacing.py b/tests/data/import_spacing.py index 51cfda23ff3..8e6e23cc348 100644 --- a/tests/data/import_spacing.py +++ b/tests/data/import_spacing.py @@ -2,6 +2,9 @@ # flake8: noqa +from logging import ( + WARNING +) from logging import ( ERROR, ) @@ -53,7 +56,10 @@ # flake8: noqa -from logging import ERROR +from logging import WARNING +from logging import ( + ERROR, +) import sys # This relies on each of the submodules having an __all__ variable. diff --git a/tests/data/long_strings.py b/tests/data/long_strings.py index 5da460b65c0..e1ed90f22de 100644 --- a/tests/data/long_strings.py +++ b/tests/data/long_strings.py @@ -137,6 +137,20 @@ ), # comment after comma ) +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), + x, + y, + z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, + y, + ("short string that should have parens stripped"), + z +) + func_with_bad_parens( ("short string that should have parens stripped"), x, @@ -487,12 +501,26 @@ def foo(): " which should NOT be there.", # comment after comma ) +func_with_bad_parens_that_wont_fit_in_one_line( + "short string that should have parens stripped", x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, "short string that should have parens stripped", z +) + func_with_bad_parens( - "short string that should have parens stripped", x, y, z, + "short string that should have parens stripped", + x, + y, + z, ) func_with_bad_parens( - x, y, "short string that should have parens stripped", z, + x, + y, + "short string that should have parens stripped", + z, ) annotated_variable: Final = ( diff --git a/tests/data/long_strings__regression.py b/tests/data/long_strings__regression.py index 8dbc58a4315..044bb4a5deb 100644 --- a/tests/data/long_strings__regression.py +++ b/tests/data/long_strings__regression.py @@ -528,17 +528,23 @@ def xxxx_xxx_xx_xxxxxxxxxx_xxxx_xxxxxxxxx(xxxx): xxxxxxxx = [ xxxxxxxxxxxxxxxx( "xxxx", - xxxxxxxxxxx={"xxxx": 1.0,}, + xxxxxxxxxxx={ + "xxxx": 1.0, + }, xxxxxx={"xxxxxx 1": xxxxxx(xxxx="xxxxxx 1", xxxxxx=600.0)}, xxxxxxxx_xxxxxxx=0.0, ), xxxxxxxxxxxxxxxx( "xxxxxxx", - xxxxxxxxxxx={"xxxx": 1.0,}, + xxxxxxxxxxx={ + "xxxx": 1.0, + }, xxxxxx={"xxxxxx 1": xxxxxx(xxxx="xxxxxx 1", xxxxxx=200.0)}, xxxxxxxx_xxxxxxx=0.0, ), - xxxxxxxxxxxxxxxx("xxxx",), + xxxxxxxxxxxxxxxx( + "xxxx", + ), ] diff --git a/tests/data/long_strings_flag_disabled.py b/tests/data/long_strings_flag_disabled.py new file mode 100644 index 00000000000..ef3094fd779 --- /dev/null +++ b/tests/data/long_strings_flag_disabled.py @@ -0,0 +1,289 @@ +x = "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +x += "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + +y = "Short string" + +print( + "This is a really long string inside of a print statement with extra arguments attached at the end of it.", + x, + y, + z, +) + +print( + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." +) + +D1 = { + "The First": "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + "The Second": "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D2 = { + 1.0: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + 2.0: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D3 = { + x: "This is a really long string that can't possibly be expected to fit all together on one line. Also it is inside a dictionary, so formatting is more difficult.", + y: "This is another really really (not really) long string that also can't be expected to fit on one line and is, like the other string, inside a dictionary.", +} + +D4 = { + "A long and ridiculous {}".format( + string_key + ): "This is a really really really long string that has to go i,side of a dictionary. It is soooo bad.", + some_func( + "calling", "some", "stuff" + ): "This is a really really really long string that has to go inside of a dictionary. It is {soooo} bad (#{x}).".format( + sooo="soooo", x=2 + ), + "A %s %s" + % ( + "formatted", + "string", + ): "This is a really really really long string that has to go inside of a dictionary. It is %s bad (#%d)." + % ("soooo", 2), +} + +func_with_keywords( + my_arg, + my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.", +) + +bad_split1 = ( + "But what should happen when code has already been formatted but in the wrong way? Like" + " with a space at the end instead of the beginning. Or what about when it is split too soon?" +) + +bad_split2 = ( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." +) + +bad_split3 = ( + "What if we have inline comments on " # First Comment + "each line of a bad split? In that " # Second Comment + "case, we should just leave it alone." # Third Comment +) + +bad_split_func1( + "But what should happen when code has already " + "been formatted but in the wrong way? Like " + "with a space at the end instead of the " + "beginning. Or what about when it is split too " + "soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split.", + xxx, + yyy, + zzz, +) + +bad_split_func2( + xxx, + yyy, + zzz, + long_string_kwarg="But what should happen when code has already been formatted but in the wrong way? Like " + "with a space at the end instead of the beginning. Or what about when it is split too " + "soon?", +) + +bad_split_func3( + ( + "But what should happen when code has already " + r"been formatted but in the wrong way? Like " + "with a space at the end instead of the " + r"beginning. Or what about when it is split too " + r"soon? In the case of a split that is too " + "short, black will try to honer the custom " + "split." + ), + xxx, + yyy, + zzz, +) + +raw_string = r"This is a long raw string. When re-formatting this string, black needs to make sure it prepends the 'r' onto the new string." + +fmt_string1 = "We also need to be sure to preserve any and all {} which may or may not be attached to the string in question.".format( + "method calls" +) + +fmt_string2 = "But what about when the string is {} but {}".format( + "short", + "the method call is really really really really really really really really long?", +) + +old_fmt_string1 = ( + "While we are on the topic of %s, we should also note that old-style formatting must also be preserved, since some %s still uses it." + % ("formatting", "code") +) + +old_fmt_string2 = "This is a %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +old_fmt_string3 = "Whereas only the strings after the percent sign were long in the last example, this example uses a long initial string as well. This is another %s %s %s %s" % ( + "really really really really really", + "old", + "way to format strings!", + "Use f-strings instead!", +) + +fstring = f"f-strings definitely make things more {difficult} than they need to be for {{black}}. But boy they sure are handy. The problem is that some lines will need to have the 'f' whereas others do not. This {line}, for example, needs one." + +fstring_with_no_fexprs = f"Some regular string that needs to get split certainly but is NOT an fstring by any means whatsoever." + +comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. + +arg_comment_string = print( + "Long lines with inline comments which are apart of (and not the only member of) an argument list should have their comments appended to the reformatted string's enclosing left parentheses.", # This comment stays on the bottom. + "Arg #2", + "Arg #3", + "Arg #4", + "Arg #5", +) + +pragma_comment_string1 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa: E501 + +pragma_comment_string2 = "Lines which end with an inline pragma comment of the form `# : <...>` should be left alone." # noqa + +"""This is a really really really long triple quote string and it should not be touched.""" + +triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception." + +assert ( + some_type_of_boolean_expression +), "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string {}.".format( + "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic string %s." + % "formatting" +) + +assert some_type_of_boolean_expression, ( + "Followed by a really really really long string that is used to provide context to the AssertionError exception, which uses dynamic %s %s." + % ("string", "formatting") +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string." +) + +some_function_call( + "With a reallly generic name and with a really really long string that is, at some point down the line, " + + added + + " to a variable and then added to another string. But then what happens when the final string is also supppppperrrrr long?! Well then that second (realllllllly long) string should be split too.", + "and a second argument", + and_a_third, +) + +return "A really really really really really really really really really really really really really long {} {}".format( + "return", "value" +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", +) + +func_with_bad_comma( + "This is a really long string argument to a function that has a trailing comma which should NOT be there.", # comment after comma +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), +) + +func_with_bad_comma( + ( + "This is a really long string argument to a function that has a trailing comma" + " which should NOT be there." + ), # comment after comma +) + +func_with_bad_parens_that_wont_fit_in_one_line( + ("short string that should have parens stripped"), x, y, z +) + +func_with_bad_parens_that_wont_fit_in_one_line( + x, y, ("short string that should have parens stripped"), z +) + +func_with_bad_parens( + ("short string that should have parens stripped"), + x, + y, + z, +) + +func_with_bad_parens( + x, + y, + ("short string that should have parens stripped"), + z, +) + +annotated_variable: Final = ( + "This is a large " + + STRING + + " that has been " + + CONCATENATED + + "using the '+' operator." +) +annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal[ + "fakse_literal" +] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" +backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" +backslashes = "This is a really 'long' string with \"embedded double quotes\" and 'single' quotes that also handles checking for an odd number of backslashes \\\", like this...\\\\\\" + +short_string = "Hi" " there." + +func_call(short_string=("Hi" " there.")) + +raw_strings = r"Don't" " get" r" merged" " unless they are all raw." + + +def foo(): + yield "This is a really long string that can't possibly be expected to fit all together on one line. In fact it may even take up three or more lines... like four or five... but probably just three." + + +x = f"This is a {{really}} long string that needs to be split without a doubt (i.e. most definitely). In short, this {string} that can't possibly be {{expected}} to fit all together on one line. In {fact} it may even take up three or more lines... like four or five... but probably just four." + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # type: ignore + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # noqa + " of it." +) + +long_unmergable_string_with_pragma = ( + "This is a really long string that can't be merged because it has a likely pragma at the end" # pylint: disable=some-pylint-check + " of it." +) diff --git a/tests/data/percent_precedence.py b/tests/data/percent_precedence.py new file mode 100644 index 00000000000..b895443fb46 --- /dev/null +++ b/tests/data/percent_precedence.py @@ -0,0 +1,41 @@ +("" % a) ** 2 +("" % a)[0] +("" % a)() +("" % a).b + +2 * ("" % a) +2 @ ("" % a) +2 / ("" % a) +2 // ("" % a) +2 % ("" % a) ++("" % a) +b + ("" % a) +-("" % a) +b - ("" % a) +b + -("" % a) +~("" % a) +2 ** ("" % a) +await ("" % a) +b[("" % a)] +b(("" % a)) +# output +("" % a) ** 2 +("" % a)[0] +("" % a)() +("" % a).b + +2 * ("" % a) +2 @ ("" % a) +2 / ("" % a) +2 // ("" % a) +2 % ("" % a) ++("" % a) +b + "" % a +-("" % a) +b - "" % a +b + -("" % a) +~("" % a) +2 ** ("" % a) +await ("" % a) +b[("" % a)] +b(("" % a)) diff --git a/tests/data/trailing_comma_optional_parens1.py b/tests/data/trailing_comma_optional_parens1.py new file mode 100644 index 00000000000..5ad29a8affd --- /dev/null +++ b/tests/data/trailing_comma_optional_parens1.py @@ -0,0 +1,3 @@ +if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT, + _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): + pass \ No newline at end of file diff --git a/tests/data/trailing_comma_optional_parens2.py b/tests/data/trailing_comma_optional_parens2.py new file mode 100644 index 00000000000..2817073816e --- /dev/null +++ b/tests/data/trailing_comma_optional_parens2.py @@ -0,0 +1,3 @@ +if (e123456.get_tk_patchlevel() >= (8, 6, 0, 'final') or + (8, 5, 8) <= get_tk_patchlevel() < (8, 6)): + pass \ No newline at end of file diff --git a/tests/data/trailing_comma_optional_parens3.py b/tests/data/trailing_comma_optional_parens3.py new file mode 100644 index 00000000000..e6a673ec537 --- /dev/null +++ b/tests/data/trailing_comma_optional_parens3.py @@ -0,0 +1,8 @@ +if True: + if True: + if True: + return _( + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweas " + + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwegqweasdzxcqweasdzxc.", + "qweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqweasdzxcqwe", + ) % {"reported_username": reported_username, "report_reason": report_reason} \ No newline at end of file diff --git a/tests/test_black.py b/tests/test_black.py index 88839d86c5a..f5d4e1115a8 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -3,14 +3,27 @@ import logging from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager +from dataclasses import replace from functools import partial +import inspect from io import BytesIO, TextIOWrapper import os from pathlib import Path import regex as re import sys from tempfile import TemporaryDirectory -from typing import Any, BinaryIO, Dict, Generator, List, Tuple, Iterator, TypeVar +import types +from typing import ( + Any, + BinaryIO, + Callable, + Dict, + Generator, + List, + Tuple, + Iterator, + TypeVar, +) import unittest from unittest.mock import patch, MagicMock @@ -36,8 +49,9 @@ from .test_primer import PrimerCLITests # noqa: F401 -ff = partial(black.format_file_in_place, mode=black.FileMode(), fast=True) -fs = partial(black.format_str, mode=black.FileMode()) +DEFAULT_MODE = black.FileMode(experimental_string_processing=True) +ff = partial(black.format_file_in_place, mode=DEFAULT_MODE, fast=True) +fs = partial(black.format_str, mode=DEFAULT_MODE) THIS_FILE = Path(__file__) THIS_DIR = THIS_FILE.parent PROJECT_ROOT = THIS_DIR.parent @@ -110,6 +124,20 @@ def skip_if_exception(e: str) -> Iterator[None]: raise +class FakeContext(click.Context): + """A fake click Context for when calling functions that need it.""" + + def __init__(self) -> None: + self.default_map: Dict[str, Any] = {} + + +class FakeParameter(click.Parameter): + """A fake click Parameter for when calling functions that need it.""" + + def __init__(self) -> None: + pass + + class BlackRunner(CliRunner): """Modify CliRunner so that stderr is not merged with stdout. @@ -137,6 +165,7 @@ def isolation(self, *args: Any, **kwargs: Any) -> Generator[BinaryIO, None, None class BlackTestCase(unittest.TestCase): maxDiff = None + _diffThreshold = 2 ** 20 def assertFormatEqual(self, expected: str, actual: str) -> None: if actual != expected and not os.environ.get("SKIP_AST_PRINT"): @@ -155,7 +184,7 @@ def assertFormatEqual(self, expected: str, actual: str) -> None: list(bdv.visit(exp_node)) except Exception as ve: black.err(str(ve)) - self.assertEqual(expected, actual) + self.assertMultiLineEqual(expected, actual) def invokeBlack( self, args: List[str], exit_code: int = 0, ignore_config: bool = True @@ -176,13 +205,13 @@ def invokeBlack( ) @patch("black.dump_to_file", dump_to_stderr) - def checkSourceFile(self, name: str) -> None: + def checkSourceFile(self, name: str, mode: black.FileMode = DEFAULT_MODE) -> None: path = THIS_DIR.parent / name source, expected = read_data(str(path), data=False) - actual = fs(source) + actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, mode) self.assertFalse(ff(path)) @patch("black.dump_to_file", dump_to_stderr) @@ -191,7 +220,7 @@ def test_empty(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_empty_ff(self) -> None: expected = "" @@ -253,7 +282,7 @@ def test_piping(self) -> None: self.assertEqual(result.exit_code, 0) self.assertFormatEqual(expected, result.output) black.assert_equivalent(source, result.output) - black.assert_stable(source, result.output, black.FileMode()) + black.assert_stable(source, result.output, DEFAULT_MODE) def test_piping_diff(self) -> None: diff_header = re.compile( @@ -306,7 +335,7 @@ def test_function(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_function2(self) -> None: @@ -314,6 +343,21 @@ def test_function2(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) + black.assert_stable(source, actual, DEFAULT_MODE) + + @patch("black.dump_to_file", dump_to_stderr) + def _test_wip(self) -> None: + source, expected = read_data("wip") + sys.settrace(tracefunc) + mode = replace( + DEFAULT_MODE, + experimental_string_processing=False, + target_versions={black.TargetVersion.PY38}, + ) + actual = fs(source, mode=mode) + sys.settrace(None) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) black.assert_stable(source, actual, black.FileMode()) @patch("black.dump_to_file", dump_to_stderr) @@ -322,7 +366,28 @@ def test_function_trailing_comma(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) + + @unittest.expectedFailure + @patch("black.dump_to_file", dump_to_stderr) + def test_trailing_comma_optional_parens_stability1(self) -> None: + source, _expected = read_data("trailing_comma_optional_parens1") + actual = fs(source) + black.assert_stable(source, actual, DEFAULT_MODE) + + @unittest.expectedFailure + @patch("black.dump_to_file", dump_to_stderr) + def test_trailing_comma_optional_parens_stability2(self) -> None: + source, _expected = read_data("trailing_comma_optional_parens2") + actual = fs(source) + black.assert_stable(source, actual, DEFAULT_MODE) + + @unittest.expectedFailure + @patch("black.dump_to_file", dump_to_stderr) + def test_trailing_comma_optional_parens_stability3(self) -> None: + source, _expected = read_data("trailing_comma_optional_parens3") + actual = fs(source) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_expression(self) -> None: @@ -330,14 +395,14 @@ def test_expression(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_pep_572(self) -> None: source, expected = read_data("pep_572") actual = fs(source) self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) if sys.version_info >= (3, 8): black.assert_equivalent(source, actual) @@ -361,7 +426,7 @@ def test_expression_ff(self) -> None: self.assertFormatEqual(expected, actual) with patch("black.dump_to_file", dump_to_stderr): black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_expression_diff(self) -> None: source, _ = read_data("expression.py") @@ -413,14 +478,14 @@ def test_fstring(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_pep_570(self) -> None: source, expected = read_data("pep_570") actual = fs(source) self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) if sys.version_info >= (3, 8): black.assert_equivalent(source, actual) @@ -438,8 +503,8 @@ def test_string_quotes(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) - mode = black.FileMode(string_normalization=False) + black.assert_stable(source, actual, DEFAULT_MODE) + mode = replace(DEFAULT_MODE, string_normalization=False) not_normalized = fs(source, mode=mode) self.assertFormatEqual(source.replace("\\\n", ""), not_normalized) black.assert_equivalent(source, not_normalized) @@ -451,7 +516,12 @@ def test_docstring(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) + mode = replace(DEFAULT_MODE, string_normalization=False) + not_normalized = fs(source, mode=mode) + self.assertFormatEqual(expected, not_normalized) + black.assert_equivalent(source, not_normalized) + black.assert_stable(source, not_normalized, mode=mode) def test_long_strings(self) -> None: """Tests for splitting long strings.""" @@ -459,7 +529,15 @@ def test_long_strings(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) + + def test_long_strings_flag_disabled(self) -> None: + """Tests for turning off the string processing logic.""" + source, expected = read_data("long_strings_flag_disabled") + mode = replace(DEFAULT_MODE, experimental_string_processing=False) + actual = fs(source, mode=mode) + self.assertFormatEqual(expected, actual) + black.assert_stable(expected, actual, mode) @patch("black.dump_to_file", dump_to_stderr) def test_long_strings__edge_case(self) -> None: @@ -468,7 +546,7 @@ def test_long_strings__edge_case(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_long_strings__regression(self) -> None: @@ -477,7 +555,7 @@ def test_long_strings__regression(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_slices(self) -> None: @@ -485,7 +563,15 @@ def test_slices(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) + + @patch("black.dump_to_file", dump_to_stderr) + def test_percent_precedence(self) -> None: + source, expected = read_data("percent_precedence") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments(self) -> None: @@ -493,7 +579,7 @@ def test_comments(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments2(self) -> None: @@ -501,7 +587,7 @@ def test_comments2(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments3(self) -> None: @@ -509,7 +595,7 @@ def test_comments3(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments4(self) -> None: @@ -517,7 +603,7 @@ def test_comments4(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments5(self) -> None: @@ -525,7 +611,7 @@ def test_comments5(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments6(self) -> None: @@ -533,15 +619,16 @@ def test_comments6(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comments7(self) -> None: source, expected = read_data("comments7") - actual = fs(source) + mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY38}) + actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_comment_after_escaped_newline(self) -> None: @@ -549,7 +636,7 @@ def test_comment_after_escaped_newline(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_cantfit(self) -> None: @@ -557,7 +644,7 @@ def test_cantfit(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_import_spacing(self) -> None: @@ -565,7 +652,7 @@ def test_import_spacing(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_composition(self) -> None: @@ -573,7 +660,16 @@ def test_composition(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) + + @patch("black.dump_to_file", dump_to_stderr) + def test_composition_no_trailing_comma(self) -> None: + source, expected = read_data("composition_no_trailing_comma") + mode = replace(DEFAULT_MODE, target_versions={black.TargetVersion.PY38}) + actual = fs(source, mode=mode) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_empty_lines(self) -> None: @@ -581,7 +677,7 @@ def test_empty_lines(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_remove_parens(self) -> None: @@ -589,7 +685,7 @@ def test_remove_parens(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_string_prefixes(self) -> None: @@ -597,12 +693,12 @@ def test_string_prefixes(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_numeric_literals(self) -> None: source, expected = read_data("numeric_literals") - mode = black.FileMode(target_versions=black.PY36_VERSIONS) + mode = replace(DEFAULT_MODE, target_versions=black.PY36_VERSIONS) actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -611,7 +707,7 @@ def test_numeric_literals(self) -> None: @patch("black.dump_to_file", dump_to_stderr) def test_numeric_literals_ignoring_underscores(self) -> None: source, expected = read_data("numeric_literals_skip_underscores") - mode = black.FileMode(target_versions=black.PY36_VERSIONS) + mode = replace(DEFAULT_MODE, target_versions=black.PY36_VERSIONS) actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -622,7 +718,7 @@ def test_numeric_literals_py2(self) -> None: source, expected = read_data("numeric_literals_py2") actual = fs(source) self.assertFormatEqual(expected, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_python2(self) -> None: @@ -630,12 +726,12 @@ def test_python2(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_python2_print_function(self) -> None: source, expected = read_data("python2_print_function") - mode = black.FileMode(target_versions={TargetVersion.PY27}) + mode = replace(DEFAULT_MODE, target_versions={TargetVersion.PY27}) actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) @@ -647,11 +743,11 @@ def test_python2_unicode_literals(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_stub(self) -> None: - mode = black.FileMode(is_pyi=True) + mode = replace(DEFAULT_MODE, is_pyi=True) source, expected = read_data("stub.pyi") actual = fs(source, mode=mode) self.assertFormatEqual(expected, actual) @@ -666,7 +762,7 @@ def test_async_as_identifier(self) -> None: major, minor = sys.version_info[:2] if major < 3 or (major <= 3 and minor < 7): black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) # ensure black can parse this when the target is 3.6 self.invokeBlack([str(source_path), "--target-version", "py36"]) # but not on 3.7, because async/await is no longer an identifier @@ -681,7 +777,7 @@ def test_python37(self) -> None: major, minor = sys.version_info[:2] if major > 3 or (major == 3 and minor >= 7): black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) # ensure black can parse this when the target is 3.7 self.invokeBlack([str(source_path), "--target-version", "py37"]) # but not on 3.6, because we use async as a reserved keyword @@ -695,7 +791,7 @@ def test_python38(self) -> None: major, minor = sys.version_info[:2] if major > 3 or (major == 3 and minor >= 8): black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff(self) -> None: @@ -703,7 +799,7 @@ def test_fmtonoff(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff2(self) -> None: @@ -711,7 +807,7 @@ def test_fmtonoff2(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff3(self) -> None: @@ -719,7 +815,7 @@ def test_fmtonoff3(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_fmtonoff4(self) -> None: @@ -727,7 +823,7 @@ def test_fmtonoff4(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_remove_empty_parentheses_after_class(self) -> None: @@ -735,7 +831,7 @@ def test_remove_empty_parentheses_after_class(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_new_line_between_class_and_code(self) -> None: @@ -743,7 +839,7 @@ def test_new_line_between_class_and_code(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_bracket_match(self) -> None: @@ -751,7 +847,7 @@ def test_bracket_match(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_tuple_assign(self) -> None: @@ -759,7 +855,7 @@ def test_tuple_assign(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) @patch("black.dump_to_file", dump_to_stderr) def test_beginning_backslash(self) -> None: @@ -767,7 +863,7 @@ def test_beginning_backslash(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_tab_comment_indentation(self) -> None: contents_tab = "if 1:\n\tif 2:\n\t\tpass\n\t# comment\n\tpass\n" @@ -1196,7 +1292,7 @@ def err(msg: str, **kwargs: Any) -> None: def test_format_file_contents(self) -> None: empty = "" - mode = black.FileMode() + mode = DEFAULT_MODE with self.assertRaises(black.NothingChanged): black.format_file_contents(empty, mode=mode, fast=False) just_nl = "\n" @@ -1241,7 +1337,7 @@ def err(msg: str, **kwargs: Any) -> None: self.assertEqual("".join(err_lines), "") def test_cache_broken_file(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace: cache_file = black.get_cache_file(mode) with cache_file.open("w") as fobj: @@ -1255,7 +1351,7 @@ def test_cache_broken_file(self) -> None: self.assertIn(src, cache) def test_cache_single_file_already_cached(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() with src.open("w") as fobj: @@ -1267,7 +1363,7 @@ def test_cache_single_file_already_cached(self) -> None: @event_loop() def test_cache_multiple_files(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace, patch( "black.ProcessPoolExecutor", new=ThreadPoolExecutor ): @@ -1288,7 +1384,7 @@ def test_cache_multiple_files(self) -> None: self.assertIn(two, cache) def test_no_cache_when_writeback_diff(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() with src.open("w") as fobj: @@ -1298,7 +1394,7 @@ def test_no_cache_when_writeback_diff(self) -> None: self.assertFalse(cache_file.exists()) def test_no_cache_when_stdin(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir(): result = CliRunner().invoke( black.main, ["-"], input=BytesIO(b"print('hello')") @@ -1308,12 +1404,12 @@ def test_no_cache_when_stdin(self) -> None: self.assertFalse(cache_file.exists()) def test_read_cache_no_cachefile(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir(): self.assertEqual(black.read_cache(mode), {}) def test_write_cache_read_cache(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace: src = (workspace / "test.py").resolve() src.touch() @@ -1339,7 +1435,7 @@ def test_filter_cached(self) -> None: self.assertEqual(done, {cached}) def test_write_cache_creates_directory_if_needed(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir(exists=False) as workspace: self.assertFalse(workspace.exists()) black.write_cache({}, [], mode) @@ -1347,7 +1443,7 @@ def test_write_cache_creates_directory_if_needed(self) -> None: @event_loop() def test_failed_formatting_does_not_get_cached(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir() as workspace, patch( "black.ProcessPoolExecutor", new=ThreadPoolExecutor ): @@ -1363,7 +1459,7 @@ def test_failed_formatting_does_not_get_cached(self) -> None: self.assertIn(clean, cache) def test_write_cache_write_fail(self) -> None: - mode = black.FileMode() + mode = DEFAULT_MODE with cache_dir(), patch.object(Path, "open") as mock: mock.side_effect = OSError black.write_cache({}, [], mode) @@ -1406,8 +1502,8 @@ def test_broken_symlink(self) -> None: self.invokeBlack([str(workspace.resolve())]) def test_read_cache_line_lengths(self) -> None: - mode = black.FileMode() - short_mode = black.FileMode(line_length=1) + mode = DEFAULT_MODE + short_mode = replace(DEFAULT_MODE, line_length=1) with cache_dir() as workspace: path = (workspace / "file.py").resolve() path.touch() @@ -1422,11 +1518,11 @@ def test_tricky_unicode_symbols(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_single_file_force_pyi(self) -> None: - reg_mode = black.FileMode() - pyi_mode = black.FileMode(is_pyi=True) + reg_mode = DEFAULT_MODE + pyi_mode = replace(DEFAULT_MODE, is_pyi=True) contents, expected = read_data("force_pyi") with cache_dir() as workspace: path = (workspace / "file.py").resolve() @@ -1444,8 +1540,8 @@ def test_single_file_force_pyi(self) -> None: @event_loop() def test_multi_file_force_pyi(self) -> None: - reg_mode = black.FileMode() - pyi_mode = black.FileMode(is_pyi=True) + reg_mode = DEFAULT_MODE + pyi_mode = replace(DEFAULT_MODE, is_pyi=True) contents, expected = read_data("force_pyi") with cache_dir() as workspace: paths = [ @@ -1477,8 +1573,8 @@ def test_pipe_force_pyi(self) -> None: self.assertFormatEqual(actual, expected) def test_single_file_force_py36(self) -> None: - reg_mode = black.FileMode() - py36_mode = black.FileMode(target_versions=black.PY36_VERSIONS) + reg_mode = DEFAULT_MODE + py36_mode = replace(DEFAULT_MODE, target_versions=black.PY36_VERSIONS) source, expected = read_data("force_py36") with cache_dir() as workspace: path = (workspace / "file.py").resolve() @@ -1496,8 +1592,8 @@ def test_single_file_force_py36(self) -> None: @event_loop() def test_multi_file_force_py36(self) -> None: - reg_mode = black.FileMode() - py36_mode = black.FileMode(target_versions=black.PY36_VERSIONS) + reg_mode = DEFAULT_MODE + py36_mode = replace(DEFAULT_MODE, target_versions=black.PY36_VERSIONS) source, expected = read_data("force_py36") with cache_dir() as workspace: paths = [ @@ -1524,7 +1620,7 @@ def test_collections(self) -> None: actual = fs(source) self.assertFormatEqual(expected, actual) black.assert_equivalent(source, actual) - black.assert_stable(source, actual, black.FileMode()) + black.assert_stable(source, actual, DEFAULT_MODE) def test_pipe_force_py36(self) -> None: source, expected = read_data("force_py36") @@ -1551,7 +1647,32 @@ def test_include_exclude(self) -> None: this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, [exclude], report, gitignore + path.iterdir(), this_abs, include, exclude, None, report, gitignore + ) + ) + self.assertEqual(sorted(expected), sorted(sources)) + + @patch("black.find_project_root", lambda *args: THIS_DIR.resolve()) + def test_exclude_for_issue_1572(self) -> None: + # Exclude shouldn't touch files that were explicitly given to Black through the + # CLI. Exclude is supposed to only apply to the recursive discovery of files. + # https://github.com/psf/black/issues/1572 + path = THIS_DIR / "data" / "include_exclude_tests" + include = "" + exclude = r"/exclude/|a\.py" + src = str(path / "b/exclude/a.py") + report = black.Report() + expected = [Path(path / "b/exclude/a.py")] + sources = list( + black.get_sources( + ctx=FakeContext(), + src=(src,), + quiet=True, + verbose=False, + include=include, + exclude=exclude, + force_exclude=None, + report=report, ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1572,7 +1693,7 @@ def test_gitignore_exclude(self) -> None: this_abs = THIS_DIR.resolve() sources.extend( black.gen_python_files( - path.iterdir(), this_abs, include, [exclude], report, gitignore + path.iterdir(), this_abs, include, exclude, None, report, gitignore ) ) self.assertEqual(sorted(expected), sorted(sources)) @@ -1600,7 +1721,8 @@ def test_empty_include(self) -> None: path.iterdir(), this_abs, empty, - [re.compile(black.DEFAULT_EXCLUDES)], + re.compile(black.DEFAULT_EXCLUDES), + None, report, gitignore, ) @@ -1627,7 +1749,8 @@ def test_empty_exclude(self) -> None: path.iterdir(), this_abs, re.compile(black.DEFAULT_INCLUDES), - [empty], + empty, + None, report, gitignore, ) @@ -1684,7 +1807,7 @@ def test_symlink_out_of_root_directory(self) -> None: try: list( black.gen_python_files( - path.iterdir(), root, include, exclude, report, gitignore + path.iterdir(), root, include, exclude, None, report, gitignore ) ) except ValueError as ve: @@ -1698,7 +1821,7 @@ def test_symlink_out_of_root_directory(self) -> None: with self.assertRaises(ValueError): list( black.gen_python_files( - path.iterdir(), root, include, exclude, report, gitignore + path.iterdir(), root, include, exclude, None, report, gitignore ) ) path.iterdir.assert_called() @@ -1777,20 +1900,8 @@ def test_parse_pyproject_toml(self) -> None: def test_read_pyproject_toml(self) -> None: test_toml_file = THIS_DIR / "test.toml" - - # Fake a click context and parameter so mypy stays happy - class FakeContext(click.Context): - def __init__(self) -> None: - self.default_map: Dict[str, Any] = {} - - class FakeParameter(click.Parameter): - def __init__(self) -> None: - pass - fake_ctx = FakeContext() - black.read_pyproject_toml( - fake_ctx, FakeParameter(), str(test_toml_file), - ) + black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file)) config = fake_ctx.default_map self.assertEqual(config["verbose"], "1") self.assertEqual(config["check"], "no") @@ -1801,6 +1912,28 @@ def __init__(self) -> None: self.assertEqual(config["exclude"], r"\.pyi?$") self.assertEqual(config["include"], r"\.py?$") + def test_find_project_root(self) -> None: + with TemporaryDirectory() as workspace: + root = Path(workspace) + test_dir = root / "test" + test_dir.mkdir() + + src_dir = root / "src" + src_dir.mkdir() + + root_pyproject = root / "pyproject.toml" + root_pyproject.touch() + src_pyproject = src_dir / "pyproject.toml" + src_pyproject.touch() + src_python = src_dir / "foo.py" + src_python.touch() + + self.assertEqual( + black.find_project_root((src_dir, test_dir)), root.resolve() + ) + self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve()) + self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve()) + class BlackDTestCase(AioHTTPTestCase): async def get_application(self) -> web.Application: @@ -1970,5 +2103,31 @@ async def test_blackd_response_black_version_header(self) -> None: self.assertIsNotNone(response.headers.get(blackd.BLACK_VERSION_HEADER)) +with open(black.__file__, "r", encoding="utf-8") as _bf: + black_source_lines = _bf.readlines() + + +def tracefunc(frame: types.FrameType, event: str, arg: Any) -> Callable: + """Show function calls `from black/__init__.py` as they happen. + + Register this with `sys.settrace()` in a test you're debugging. + """ + if event != "call": + return tracefunc + + stack = len(inspect.stack()) - 19 + stack *= 2 + filename = frame.f_code.co_filename + lineno = frame.f_lineno + func_sig_lineno = lineno - 1 + funcname = black_source_lines[func_sig_lineno].strip() + while funcname.startswith("@"): + func_sig_lineno += 1 + funcname = black_source_lines[func_sig_lineno].strip() + if "black/__init__.py" in filename: + print(f"{' ' * stack}{lineno}:{funcname}") + return tracefunc + + if __name__ == "__main__": unittest.main(module="test_black")